Completed
Branch master (5cb30d)
by
unknown
02:00
created

BlocktrailSDK::getPathForAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
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\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 92
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
59
60 92
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
61
62 92
        if (is_null($apiEndpoint)) {
63 92
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
64 92
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
65
        }
66
67
        // normalize network and set bitcoinlib to the right magic-bytes
68 92
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
69 92
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
70
71 92
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
72 92
    }
73
74
    /**
75
     * normalize network string
76
     *
77
     * @param $network
78
     * @param $testnet
79
     * @return array
80
     * @throws \Exception
81
     */
82 92
    protected function normalizeNetwork($network, $testnet) {
83 92
        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 92
    protected function setBitcoinLibMagicBytes($network, $testnet) {
93 92
        assert($network == "bitcoin" || $network == "bitcoincash");
94 92
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
95 92
    }
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 5
    public function transaction($txhash) {
295 5
        $response = $this->client->get("transaction/{$txhash}");
296 5
        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(
633 1
            $options['identifier'],
634 1
            $primaryPublicKey->tuple(),
635 1
            $backupPublicKey->tuple(),
636 1
            $primaryMnemonic,
637 1
            $checksum,
638 1
            $options['key_index'],
639 1
            array_key_exists('segwit', $options) ? $options['segwit'] : false
640
        );
641
642
        // received the blocktrail public keys
643
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
644 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
645 1
        }, $data['blocktrail_public_keys']);
646
647 1
        $wallet = new WalletV1(
648 1
            $this,
649 1
            $options['identifier'],
650 1
            $primaryMnemonic,
651 1
            [$options['key_index'] => $primaryPublicKey],
652 1
            $backupPublicKey,
653 1
            $blocktrailPublicKeys,
654 1
            $options['key_index'],
655 1
            $this->network,
656 1
            $this->testnet,
657 1
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
658 1
            $checksum
659
        );
660
661 1
        $wallet->unlock($options);
662
663
        // return wallet and backup mnemonic
664
        return [
665 1
            $wallet,
666
            [
667 1
                'primary_mnemonic' => $primaryMnemonic,
668 1
                'backup_mnemonic' => $backupMnemonic,
669 1
                'blocktrail_public_keys' => $blocktrailPublicKeys,
670
            ],
671
        ];
672
    }
673
674 5
    public static function randomBits($bits) {
675 5
        return self::randomBytes($bits / 8);
676
    }
677
678 5
    public static function randomBytes($bytes) {
679 5
        return (new Random())->bytes($bytes)->getBinary();
680
    }
681
682 2
    protected function createNewWalletV2($options) {
683 2
        $walletPath = WalletPath::create($options['key_index']);
684
685 2
        if (isset($options['store_primary_mnemonic'])) {
686
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
687
        }
688
689 2
        if (!isset($options['store_data_on_server'])) {
690 2
            if (isset($options['primary_private_key'])) {
691 1
                $options['store_data_on_server'] = false;
692
            } else {
693 1
                $options['store_data_on_server'] = true;
694
            }
695
        }
696
697 2
        $storeDataOnServer = $options['store_data_on_server'];
698
699 2
        $secret = null;
700 2
        $encryptedSecret = null;
701 2
        $primarySeed = null;
702 2
        $encryptedPrimarySeed = null;
703 2
        $recoverySecret = null;
704 2
        $recoveryEncryptedSecret = null;
705 2
        $backupSeed = null;
706
707 2
        if (!isset($options['primary_private_key'])) {
708 1
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
709
        }
710
711 2
        if ($storeDataOnServer) {
712 1
            if (!isset($options['secret'])) {
713 1
                if (!$options['passphrase']) {
714
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
715
                }
716
717 1
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
718 1
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
719
            } else {
720
                $secret = $options['secret'];
721
            }
722
723 1
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
724 1
            $recoverySecret = bin2hex(self::randomBits(256));
725
726 1
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
727
        }
728
729 2
        if (!isset($options['backup_public_key'])) {
730 1
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
731
        }
732
733 2
        if (isset($options['primary_private_key'])) {
734 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
735
        } else {
736 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
737
        }
738
739
        // create primary public key from the created private key
740 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
741
742 2
        if (!isset($options['backup_public_key'])) {
743 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
744
        }
745
746
        // create a checksum of our private key which we'll later use to verify we used the right password
747 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
748
749
        // send the public keys and encrypted data to server
750 2
        $data = $this->storeNewWalletV2(
751 2
            $options['identifier'],
752 2
            $options['primary_public_key']->tuple(),
753 2
            $options['backup_public_key']->tuple(),
754 2
            $storeDataOnServer ? $encryptedPrimarySeed : false,
755 2
            $storeDataOnServer ? $encryptedSecret : false,
756 2
            $storeDataOnServer ? $recoverySecret : false,
757 2
            $checksum,
758 2
            $options['key_index'],
759 2
            array_key_exists('segwit', $options) ? $options['segwit'] : false
760
        );
761
762
        // received the blocktrail public keys
763
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
764 2
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
765 2
        }, $data['blocktrail_public_keys']);
766
767 2
        $wallet = new WalletV2(
768 2
            $this,
769 2
            $options['identifier'],
770 2
            $encryptedPrimarySeed,
771 2
            $encryptedSecret,
772 2
            [$options['key_index'] => $options['primary_public_key']],
773 2
            $options['backup_public_key'],
774 2
            $blocktrailPublicKeys,
775 2
            $options['key_index'],
776 2
            $this->network,
777 2
            $this->testnet,
778 2
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
779 2
            $checksum
780
        );
781
782 2
        $wallet->unlock([
783 2
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
784 2
            'primary_private_key' => $options['primary_private_key'],
785 2
            'primary_seed' => $primarySeed,
786 2
            'secret' => $secret,
787
        ]);
788
789
        // return wallet and mnemonics for backup sheet
790
        return [
791 2
            $wallet,
792
            [
793 2
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
794 2
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
795 2
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
796 2
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
797
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
798 2
                    return [$keyIndex, $pubKey->tuple()];
799 2
                }, $blocktrailPublicKeys),
800
            ],
801
        ];
802
    }
803
804 4
    protected function createNewWalletV3($options) {
805 4
        $walletPath = WalletPath::create($options['key_index']);
806
807 4
        if (isset($options['store_primary_mnemonic'])) {
808
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
809
        }
810
811 4
        if (!isset($options['store_data_on_server'])) {
812 4
            if (isset($options['primary_private_key'])) {
813
                $options['store_data_on_server'] = false;
814
            } else {
815 4
                $options['store_data_on_server'] = true;
816
            }
817
        }
818
819 4
        $storeDataOnServer = $options['store_data_on_server'];
820
821 4
        $secret = null;
822 4
        $encryptedSecret = null;
823 4
        $primarySeed = null;
824 4
        $encryptedPrimarySeed = null;
825 4
        $recoverySecret = null;
826 4
        $recoveryEncryptedSecret = null;
827 4
        $backupSeed = null;
828
829 4
        if (!isset($options['primary_private_key'])) {
830 4
            if (isset($options['primary_seed'])) {
831
                if (!$options['primary_seed'] instanceof BufferInterface) {
832
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
833
                }
834
                $primarySeed = $options['primary_seed'];
835
            } else {
836 4
                $primarySeed = new Buffer(self::randomBits(256));
837
            }
838
        }
839
840 4
        if ($storeDataOnServer) {
841 4
            if (!isset($options['secret'])) {
842 4
                if (!$options['passphrase']) {
843
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
844
                }
845
846 4
                $secret = new Buffer(self::randomBits(256));
847 4
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
848
            } else {
849
                if (!$options['secret'] instanceof Buffer) {
850
                    throw new \RuntimeException('Secret must be provided as a Buffer');
851
                }
852
853
                $secret = $options['secret'];
854
            }
855
856 4
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
857 4
            $recoverySecret = new Buffer(self::randomBits(256));
858
859 4
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
860
        }
861
862 4
        if (!isset($options['backup_public_key'])) {
863 4
            if (isset($options['backup_seed'])) {
864
                if (!$options['backup_seed'] instanceof Buffer) {
865
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
866
                }
867
                $backupSeed = $options['backup_seed'];
868
            } else {
869 4
                $backupSeed = new Buffer(self::randomBits(256));
870
            }
871
        }
872
873 4
        if (isset($options['primary_private_key'])) {
874
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
875
        } else {
876 4
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
877
        }
878
879
        // create primary public key from the created private key
880 4
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
881
882 4
        if (!isset($options['backup_public_key'])) {
883 4
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
884
        }
885
886
        // create a checksum of our private key which we'll later use to verify we used the right password
887 4
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
888
889
        // send the public keys and encrypted data to server
890 4
        $data = $this->storeNewWalletV3(
891 4
            $options['identifier'],
892 4
            $options['primary_public_key']->tuple(),
893 4
            $options['backup_public_key']->tuple(),
894 4
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
895 4
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
896 4
            $storeDataOnServer ? $recoverySecret->getHex() : false,
897 4
            $checksum,
898 4
            $options['key_index'],
899 4
            array_key_exists('segwit', $options) ? $options['segwit'] : false
900
        );
901
902
        // received the blocktrail public keys
903
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
904 4
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
905 4
        }, $data['blocktrail_public_keys']);
906
907 4
        $wallet = new WalletV3(
908 4
            $this,
909 4
            $options['identifier'],
910 4
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 824 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...
911 4
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 822 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...
912 4
            [$options['key_index'] => $options['primary_public_key']],
913 4
            $options['backup_public_key'],
914 4
            $blocktrailPublicKeys,
915 4
            $options['key_index'],
916 4
            $this->network,
917 4
            $this->testnet,
918 4
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
919 4
            $checksum
920
        );
921
922 4
        $wallet->unlock([
923 4
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
924 4
            'primary_private_key' => $options['primary_private_key'],
925 4
            'primary_seed' => $primarySeed,
926 4
            'secret' => $secret,
927
        ]);
928
929
        // return wallet and mnemonics for backup sheet
930
        return [
931 4
            $wallet,
932
            [
933 4
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
934 4
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
935 4
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
936 4
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
937
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
938 4
                    return [$keyIndex, $pubKey->tuple()];
939 4
                }, $blocktrailPublicKeys),
940
            ]
941
        ];
942
    }
943
944
    /**
945
     * @param array $bip32Key
946
     * @throws BlocktrailSDKException
947
     */
948 10
    private function verifyPublicBIP32Key(array $bip32Key) {
949 10
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
950 10
        if ($hk->isPrivate()) {
951
            throw new BlocktrailSDKException('Private key was included in request, abort');
952
        }
953
954 10
        if (substr($bip32Key[1], 0, 1) === "m") {
955
            throw new BlocktrailSDKException("Private path was included in the request, abort");
956
        }
957 10
    }
958
959
    /**
960
     * @param array $walletData
961
     * @throws BlocktrailSDKException
962
     */
963 10
    private function verifyPublicOnly(array $walletData) {
964 10
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
965 10
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
966 10
    }
967
968
    /**
969
     * create wallet using the API
970
     *
971
     * @param string    $identifier             the wallet identifier to create
972
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
973
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
974
     * @param string    $primaryMnemonic        mnemonic to store
975
     * @param string    $checksum               checksum to store
976
     * @param int       $keyIndex               account that we expect to use
977
     * @param bool      $segwit                 opt in to segwit
978
     * @return mixed
979
     */
980 1
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
981
        $data = [
982 1
            'identifier' => $identifier,
983 1
            'primary_public_key' => $primaryPublicKey,
984 1
            'backup_public_key' => $backupPublicKey,
985 1
            'primary_mnemonic' => $primaryMnemonic,
986 1
            'checksum' => $checksum,
987 1
            'key_index' => $keyIndex,
988 1
            'segwit' => $segwit,
989
        ];
990 1
        $this->verifyPublicOnly($data);
991 1
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
992 1
        return self::jsonDecode($response->body(), true);
993
    }
994
995
    /**
996
     * create wallet using the API
997
     *
998
     * @param string $identifier       the wallet identifier to create
999
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1000
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1001
     * @param        $encryptedPrimarySeed
1002
     * @param        $encryptedSecret
1003
     * @param        $recoverySecret
1004
     * @param string $checksum         checksum to store
1005
     * @param int    $keyIndex         account that we expect to use
1006
     * @param bool   $segwit           opt in to segwit
1007
     * @return mixed
1008
     * @throws \Exception
1009
     */
1010 5
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1011
        $data = [
1012 5
            'identifier' => $identifier,
1013
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1014 5
            'primary_public_key' => $primaryPublicKey,
1015 5
            'backup_public_key' => $backupPublicKey,
1016 5
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1017 5
            'encrypted_secret' => $encryptedSecret,
1018 5
            'recovery_secret' => $recoverySecret,
1019 5
            'checksum' => $checksum,
1020 5
            'key_index' => $keyIndex,
1021 5
            'segwit' => $segwit,
1022
        ];
1023 5
        $this->verifyPublicOnly($data);
1024 5
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1025 5
        return self::jsonDecode($response->body(), true);
1026
    }
1027
1028
    /**
1029
     * create wallet using the API
1030
     *
1031
     * @param string $identifier       the wallet identifier to create
1032
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1033
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1034
     * @param        $encryptedPrimarySeed
1035
     * @param        $encryptedSecret
1036
     * @param        $recoverySecret
1037
     * @param string $checksum         checksum to store
1038
     * @param int    $keyIndex         account that we expect to use
1039
     * @param bool   $segwit           opt in to segwit
1040
     * @return mixed
1041
     * @throws \Exception
1042
     */
1043 4
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1044
1045
        $data = [
1046 4
            'identifier' => $identifier,
1047
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1048 4
            'primary_public_key' => $primaryPublicKey,
1049 4
            'backup_public_key' => $backupPublicKey,
1050 4
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1051 4
            'encrypted_secret' => $encryptedSecret,
1052 4
            'recovery_secret' => $recoverySecret,
1053 4
            'checksum' => $checksum,
1054 4
            'key_index' => $keyIndex,
1055 4
            'segwit' => $segwit,
1056
        ];
1057
1058 4
        $this->verifyPublicOnly($data);
1059 4
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1060 4
        return self::jsonDecode($response->body(), true);
1061
    }
1062
1063
    /**
1064
     * upgrade wallet to use a new account number
1065
     *  the account number specifies which blocktrail cosigning key is used
1066
     *
1067
     * @param string    $identifier             the wallet identifier to be upgraded
1068
     * @param int       $keyIndex               the new account to use
1069
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1070
     * @return mixed
1071
     */
1072 3
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1073
        $data = [
1074 3
            'key_index' => $keyIndex,
1075 3
            'primary_public_key' => $primaryPublicKey
1076
        ];
1077
1078 3
        $response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1079 3
        return self::jsonDecode($response->body(), true);
1080
    }
1081
1082
    /**
1083
     * initialize a previously created wallet
1084
     *
1085
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1086
     *
1087
     * Some of the options:
1088
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1089
     *    so the wallet is loaded in read-only mode (no private key)
1090
     *  - "check_backup_key" can be set to your own backup key:
1091
     *    Format: ["M', "xpub..."]
1092
     *    Setting this will allow the SDK to check the server hasn't
1093
     *    a different key (one it happens to control)
1094
1095
     * Either takes one argument:
1096
     * @param array $options
1097
     *
1098
     * Or takes two arguments (old, deprecated syntax):
1099
     * (@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...
1100
     * (@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...
1101
     *
1102
     * @return WalletInterface
1103
     * @throws \Exception
1104
     */
1105 19
    public function initWallet($options) {
1106 19
        if (!is_array($options)) {
1107 1
            $args = func_get_args();
1108
            $options = [
1109 1
                "identifier" => $args[0],
1110 1
                "password" => $args[1],
1111
            ];
1112
        }
1113
1114 19
        $identifier = $options['identifier'];
1115 19
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1116 19
                    (isset($options['readOnly']) ? $options['readOnly'] :
1117 19
                        (isset($options['read-only']) ? $options['read-only'] :
1118 19
                            false));
1119
1120
        // get the wallet data from the server
1121 19
        $data = $this->getWallet($identifier);
1122 19
        if (!$data) {
1123
            throw new \Exception("Failed to get wallet");
1124
        }
1125
1126 19
        if (array_key_exists('check_backup_key', $options)) {
1127 1
            if (!is_string($options['check_backup_key'])) {
1128 1
                throw new \RuntimeException("check_backup_key should be a string (the xpub)");
1129
            }
1130 1
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1131 1
                throw new \RuntimeException("Backup key returned from server didn't match our own");
1132
            }
1133
        }
1134
1135 19
        switch ($data['wallet_version']) {
1136 19
            case Wallet::WALLET_VERSION_V1:
1137 13
                $wallet = new WalletV1(
1138 13
                    $this,
1139 13
                    $identifier,
1140 13
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1141 13
                    $data['primary_public_keys'],
1142 13
                    $data['backup_public_key'],
1143 13
                    $data['blocktrail_public_keys'],
1144 13
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1145 13
                    $this->network,
1146 13
                    $this->testnet,
1147 13
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1148 13
                    $data['checksum']
1149
                );
1150 13
                break;
1151 6
            case Wallet::WALLET_VERSION_V2:
1152 2
                $wallet = new WalletV2(
1153 2
                    $this,
1154 2
                    $identifier,
1155 2
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1156 2
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1157 2
                    $data['primary_public_keys'],
1158 2
                    $data['backup_public_key'],
1159 2
                    $data['blocktrail_public_keys'],
1160 2
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1161 2
                    $this->network,
1162 2
                    $this->testnet,
1163 2
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1164 2
                    $data['checksum']
1165
                );
1166 2
                break;
1167 4
            case Wallet::WALLET_VERSION_V3:
1168 4
                if (isset($options['encrypted_primary_seed'])) {
1169
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1170
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1171
                    }
1172
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1173
                } else {
1174 4
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1175
                }
1176
1177 4
                if (isset($options['encrypted_secret'])) {
1178
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1179
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1180
                    }
1181
1182
                    $encryptedSecret = $data['encrypted_secret'];
1183
                } else {
1184 4
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1185
                }
1186
1187 4
                $wallet = new WalletV3(
1188 4
                    $this,
1189 4
                    $identifier,
1190 4
                    $encryptedPrimarySeed,
1191 4
                    $encryptedSecret,
1192 4
                    $data['primary_public_keys'],
1193 4
                    $data['backup_public_key'],
1194 4
                    $data['blocktrail_public_keys'],
1195 4
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1196 4
                    $this->network,
1197 4
                    $this->testnet,
1198 4
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1199 4
                    $data['checksum']
1200
                );
1201 4
                break;
1202
            default:
1203
                throw new \InvalidArgumentException("Invalid wallet version");
1204
        }
1205
1206 19
        if (!$readonly) {
1207 19
            $wallet->unlock($options);
1208
        }
1209
1210 19
        return $wallet;
1211
    }
1212
1213
    /**
1214
     * get the wallet data from the server
1215
     *
1216
     * @param string    $identifier             the identifier of the wallet
1217
     * @return mixed
1218
     */
1219 19
    public function getWallet($identifier) {
1220 19
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1221 19
        return self::jsonDecode($response->body(), true);
1222
    }
1223
1224
    /**
1225
     * update the wallet data on the server
1226
     *
1227
     * @param string    $identifier
1228
     * @param $data
1229
     * @return mixed
1230
     */
1231 2
    public function updateWallet($identifier, $data) {
1232 2
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1233 2
        return self::jsonDecode($response->body(), true);
1234
    }
1235
1236
    /**
1237
     * delete a wallet from the server
1238
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1239
     *  is required to be able to delete a wallet
1240
     *
1241
     * @param string    $identifier             the identifier of the wallet
1242
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1243
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1244
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1245
     * @return mixed
1246
     */
1247 10
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1248 10
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
1249 10
            'checksum' => $checksumAddress,
1250 10
            'signature' => $signature
1251 10
        ], RestClient::AUTH_HTTP_SIG, 360);
1252 10
        return self::jsonDecode($response->body(), true);
1253
    }
1254
1255
    /**
1256
     * create new backup key;
1257
     *  1) a BIP39 mnemonic
1258
     *  2) a seed from that mnemonic with a blank password
1259
     *  3) a private key from that seed
1260
     *
1261
     * @return array [mnemonic, seed, key]
1262
     */
1263 1
    protected function newBackupSeed() {
1264 1
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1265
1266 1
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1267
    }
1268
1269
    /**
1270
     * create new primary key;
1271
     *  1) a BIP39 mnemonic
1272
     *  2) a seed from that mnemonic with the password
1273
     *  3) a private key from that seed
1274
     *
1275
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1276
     * @return array [mnemonic, seed, key]
1277
     * @TODO: require a strong password?
1278
     */
1279 1
    protected function newPrimarySeed($passphrase) {
1280 1
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1281
1282 1
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1283
    }
1284
1285
    /**
1286
     * create a new key;
1287
     *  1) a BIP39 mnemonic
1288
     *  2) a seed from that mnemonic with the password
1289
     *  3) a private key from that seed
1290
     *
1291
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1292
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1293
     * @return array
1294
     */
1295 1
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1296
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1297
        do {
1298 1
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1299
1300 1
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1301
1302 1
            $key = null;
1303
            try {
1304 1
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1305
            } catch (\Exception $e) {
1306
                // try again
1307
            }
1308 1
        } while (!$key);
1309
1310 1
        return [$mnemonic, $seed, $key];
1311
    }
1312
1313
    /**
1314
     * generate a new mnemonic from some random entropy (512 bit)
1315
     *
1316
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1317
     * @return string
1318
     * @throws \Exception
1319
     */
1320 1
    protected function generateNewMnemonic($forceEntropy = null) {
1321 1
        if ($forceEntropy === null) {
1322 1
            $random = new Random();
1323 1
            $entropy = $random->bytes(512 / 8);
1324
        } else {
1325
            $entropy = $forceEntropy;
1326
        }
1327
1328 1
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1325 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...
1329
    }
1330
1331
    /**
1332
     * get the balance for the wallet
1333
     *
1334
     * @param string    $identifier             the identifier of the wallet
1335
     * @return array
1336
     */
1337 9
    public function getWalletBalance($identifier) {
1338 9
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1339 9
        return self::jsonDecode($response->body(), true);
1340
    }
1341
1342
    /**
1343
     * do HD wallet discovery for the wallet
1344
     *
1345
     * this can be REALLY slow, so we've set the timeout to 120s ...
1346
     *
1347
     * @param string    $identifier             the identifier of the wallet
1348
     * @param int       $gap                    the gap setting to use for discovery
1349
     * @return mixed
1350
     */
1351 2
    public function doWalletDiscovery($identifier, $gap = 200) {
1352 2
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1353 1
        return self::jsonDecode($response->body(), true);
1354
    }
1355
1356
    /**
1357
     * get a new derivation number for specified parent path
1358
     *  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
1359
     *
1360
     * returns the path
1361
     *
1362
     * @param string    $identifier             the identifier of the wallet
1363
     * @param string    $path                   the parent path for which to get a new derivation
1364
     * @return string
1365
     */
1366 1
    public function getNewDerivation($identifier, $path) {
1367 1
        $result = $this->_getNewDerivation($identifier, $path);
1368 1
        return $result['path'];
1369
    }
1370
1371
    /**
1372
     * get a new derivation number for specified parent path
1373
     *  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
1374
     *
1375
     * @param string    $identifier             the identifier of the wallet
1376
     * @param string    $path                   the parent path for which to get a new derivation
1377
     * @return mixed
1378
     */
1379 14
    public function _getNewDerivation($identifier, $path) {
1380 14
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1381 14
        return self::jsonDecode($response->body(), true);
1382
    }
1383
1384
    /**
1385
     * get the path (and redeemScript) to specified address
1386
     *
1387
     * @param string $identifier
1388
     * @param string $address
1389
     * @return array
1390
     * @throws \Exception
1391
     */
1392 1
    public function getPathForAddress($identifier, $address) {
1393 1
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1394 1
        return self::jsonDecode($response->body(), true)['path'];
1395
    }
1396
1397
    /**
1398
     * send the transaction using the API
1399
     *
1400
     * @param string         $identifier             the identifier of the wallet
1401
     * @param string|array   $rawTransaction         raw hex of the transaction (should be partially signed)
1402
     * @param array          $paths                  list of the paths that were used for the UTXO
1403
     * @param bool           $checkFee               let the server verify the fee after signing
1404
     * @return string                                the complete raw transaction
1405
     * @throws \Exception
1406
     */
1407 4
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1408
        $data = [
1409 4
            'paths' => $paths
1410
        ];
1411
1412 4
        if (is_array($rawTransaction)) {
1413 4
            if (array_key_exists('base_transaction', $rawTransaction)
1414 4
            && array_key_exists('signed_transaction', $rawTransaction)) {
1415 4
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1416 4
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1417
            } else {
1418 4
                throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1419
            }
1420
        } else {
1421
            $data['raw_transaction'] = $rawTransaction;
1422
        }
1423
1424
        // dynamic TTL for when we're signing really big transactions
1425 4
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1426
1427 4
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1428 4
        $signed = self::jsonDecode($response->body(), true);
1429
1430 4
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1431
            throw new \Exception("Failed to completely sign transaction");
1432
        }
1433
1434
        // create TX hash from the raw signed hex
1435 4
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1436
    }
1437
1438
    /**
1439
     * use the API to get the best inputs to use based on the outputs
1440
     *
1441
     * the return array has the following format:
1442
     * [
1443
     *  "utxos" => [
1444
     *      [
1445
     *          "hash" => "<txHash>",
1446
     *          "idx" => "<index of the output of that <txHash>",
1447
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1448
     *          "value" => 32746327,
1449
     *          "address" => "1address",
1450
     *          "path" => "m/44'/1'/0'/0/13",
1451
     *          "redeem_script" => "<redeemScript-hex>",
1452
     *      ],
1453
     *  ],
1454
     *  "fee"   => 10000,
1455
     *  "change"=> 1010109201,
1456
     * ]
1457
     *
1458
     * @param string   $identifier              the identifier of the wallet
1459
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1460
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1461
     *                                          so you have some time to spend them without race-conditions
1462
     * @param bool     $allowZeroConf
1463
     * @param string   $feeStrategy
1464
     * @param null|int $forceFee
1465
     * @return array
1466
     * @throws \Exception
1467
     */
1468 10
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1469
        $args = [
1470 10
            'lock' => (int)!!$lockUTXO,
1471 10
            'zeroconf' => (int)!!$allowZeroConf,
1472 10
            'fee_strategy' => $feeStrategy,
1473
        ];
1474
1475 10
        if ($forceFee !== null) {
1476
            $args['forcefee'] = (int)$forceFee;
1477
        }
1478
1479 10
        $response = $this->client->post(
1480 10
            "wallet/{$identifier}/coin-selection",
1481 10
            $args,
1482 10
            $outputs,
1483 10
            RestClient::AUTH_HTTP_SIG
1484
        );
1485
1486 5
        return self::jsonDecode($response->body(), true);
1487
    }
1488
1489
    /**
1490
     *
1491
     * @param string   $identifier the identifier of the wallet
1492
     * @param bool     $allowZeroConf
1493
     * @param string   $feeStrategy
1494
     * @param null|int $forceFee
1495
     * @param int      $outputCnt
1496
     * @return array
1497
     * @throws \Exception
1498
     */
1499
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1500
        $args = [
1501
            'zeroconf' => (int)!!$allowZeroConf,
1502
            'fee_strategy' => $feeStrategy,
1503
            'outputs' => $outputCnt,
1504
        ];
1505
1506
        if ($forceFee !== null) {
1507
            $args['forcefee'] = (int)$forceFee;
1508
        }
1509
1510
        $response = $this->client->get(
1511
            "wallet/{$identifier}/max-spendable",
1512
            $args,
1513
            RestClient::AUTH_HTTP_SIG
1514
        );
1515
1516
        return self::jsonDecode($response->body(), true);
1517
    }
1518
1519
    /**
1520
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1521
     */
1522 3
    public function feePerKB() {
1523 3
        $response = $this->client->get("fee-per-kb");
1524 3
        return self::jsonDecode($response->body(), true);
1525
    }
1526
1527
    /**
1528
     * get the current price index
1529
     *
1530
     * @return array        eg; ['USD' => 287.30]
1531
     */
1532 1
    public function price() {
1533 1
        $response = $this->client->get("price");
1534 1
        return self::jsonDecode($response->body(), true);
1535
    }
1536
1537
    /**
1538
     * setup webhook for wallet
1539
     *
1540
     * @param string    $identifier         the wallet identifier for which to create the webhook
1541
     * @param string    $webhookIdentifier  the webhook identifier to use
1542
     * @param string    $url                the url to receive the webhook events
1543
     * @return array
1544
     */
1545 1
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1546 1
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1547 1
        return self::jsonDecode($response->body(), true);
1548
    }
1549
1550
    /**
1551
     * delete webhook for wallet
1552
     *
1553
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1554
     * @param string    $webhookIdentifier  the webhook identifier to delete
1555
     * @return array
1556
     */
1557 1
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1558 1
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1559 1
        return self::jsonDecode($response->body(), true);
1560
    }
1561
1562
    /**
1563
     * lock a specific unspent output
1564
     *
1565
     * @param     $identifier
1566
     * @param     $txHash
1567
     * @param     $txIdx
1568
     * @param int $ttl
1569
     * @return bool
1570
     */
1571
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1572
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1573
        return self::jsonDecode($response->body(), true)['locked'];
1574
    }
1575
1576
    /**
1577
     * unlock a specific unspent output
1578
     *
1579
     * @param     $identifier
1580
     * @param     $txHash
1581
     * @param     $txIdx
1582
     * @return bool
1583
     */
1584
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1585
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1586
        return self::jsonDecode($response->body(), true)['unlocked'];
1587
    }
1588
1589
    /**
1590
     * get all transactions for wallet (paginated)
1591
     *
1592
     * @param  string  $identifier  the wallet identifier for which to get transactions
1593
     * @param  integer $page        pagination: page number
1594
     * @param  integer $limit       pagination: records per page (max 500)
1595
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1596
     * @return array                associative array containing the response
1597
     */
1598 1
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1599
        $queryString = [
1600 1
            'page' => $page,
1601 1
            'limit' => $limit,
1602 1
            'sort_dir' => $sortDir
1603
        ];
1604 1
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1605 1
        return self::jsonDecode($response->body(), true);
1606
    }
1607
1608
    /**
1609
     * get all addresses for wallet (paginated)
1610
     *
1611
     * @param  string  $identifier  the wallet identifier for which to get addresses
1612
     * @param  integer $page        pagination: page number
1613
     * @param  integer $limit       pagination: records per page (max 500)
1614
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1615
     * @return array                associative array containing the response
1616
     */
1617 1
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1618
        $queryString = [
1619 1
            'page' => $page,
1620 1
            'limit' => $limit,
1621 1
            'sort_dir' => $sortDir
1622
        ];
1623 1
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1624 1
        return self::jsonDecode($response->body(), true);
1625
    }
1626
1627
    /**
1628
     * get all UTXOs for wallet (paginated)
1629
     *
1630
     * @param  string  $identifier  the wallet identifier for which to get addresses
1631
     * @param  integer $page        pagination: page number
1632
     * @param  integer $limit       pagination: records per page (max 500)
1633
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1634
     * @param  boolean $zeroconf    include zero confirmation transactions
1635
     * @return array                associative array containing the response
1636
     */
1637 1
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1638
        $queryString = [
1639 1
            'page' => $page,
1640 1
            'limit' => $limit,
1641 1
            'sort_dir' => $sortDir,
1642 1
            'zeroconf' => (int)!!$zeroconf,
1643
        ];
1644 1
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1645 1
        return self::jsonDecode($response->body(), true);
1646
    }
1647
1648
    /**
1649
     * get a paginated list of all wallets associated with the api user
1650
     *
1651
     * @param  integer          $page    pagination: page number
1652
     * @param  integer          $limit   pagination: records per page
1653
     * @return array                     associative array containing the response
1654
     */
1655 2
    public function allWallets($page = 1, $limit = 20) {
1656
        $queryString = [
1657 2
            'page' => $page,
1658 2
            'limit' => $limit
1659
        ];
1660 2
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1661 2
        return self::jsonDecode($response->body(), true);
1662
    }
1663
1664
    /**
1665
     * send raw transaction
1666
     *
1667
     * @param     $txHex
1668
     * @return bool
1669
     */
1670
    public function sendRawTransaction($txHex) {
1671
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1672
        return self::jsonDecode($response->body(), true);
1673
    }
1674
1675
    /**
1676
     * testnet only ;-)
1677
     *
1678
     * @param     $address
1679
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1680
     * @return mixed
1681
     * @throws \Exception
1682
     */
1683
    public function faucetWithdrawal($address, $amount = 10000) {
1684
        $response = $this->client->post("faucet/withdrawl", null, [
1685
            'address' => $address,
1686
            'amount' => $amount,
1687
        ], RestClient::AUTH_HTTP_SIG);
1688
        return self::jsonDecode($response->body(), true);
1689
    }
1690
1691
    /**
1692
     * Exists for BC. Remove at major bump.
1693
     *
1694
     * @see faucetWithdrawal
1695
     * @deprecated
1696
     * @param     $address
1697
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1698
     * @return mixed
1699
     * @throws \Exception
1700
     */
1701
    public function faucetWithdrawl($address, $amount = 10000) {
1702
        return $this->faucetWithdrawal($address, $amount);
1703
    }
1704
1705
    /**
1706
     * verify a message signed bitcoin-core style
1707
     *
1708
     * @param  string           $message
1709
     * @param  string           $address
1710
     * @param  string           $signature
1711
     * @return boolean
1712
     */
1713 1
    public function verifyMessage($message, $address, $signature) {
1714
        // we could also use the API instead of the using BitcoinLib to verify
1715
        // $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...
1716
1717 1
        $adapter = Bitcoin::getEcAdapter();
1718 1
        $addr = AddressFactory::fromString($address);
1719 1
        if (!$addr instanceof PayToPubKeyHashAddress) {
1720
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1721
        }
1722
1723
        /** @var CompactSignatureSerializerInterface $csSerializer */
1724 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...
1725 1
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1726
1727 1
        $signer = new MessageSigner($adapter);
1728 1
        return $signer->verify($signedMessage, $addr);
1729
    }
1730
1731
    /**
1732
     * convert a Satoshi value to a BTC value
1733
     *
1734
     * @param int       $satoshi
1735
     * @return float
1736
     */
1737
    public static function toBTC($satoshi) {
1738
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1739
    }
1740
1741
    /**
1742
     * convert a Satoshi value to a BTC value and return it as a string
1743
1744
     * @param int       $satoshi
1745
     * @return string
1746
     */
1747
    public static function toBTCString($satoshi) {
1748
        return sprintf("%.8f", self::toBTC($satoshi));
1749
    }
1750
1751
    /**
1752
     * convert a BTC value to a Satoshi value
1753
     *
1754
     * @param float     $btc
1755
     * @return string
1756
     */
1757 12
    public static function toSatoshiString($btc) {
1758 12
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1759
    }
1760
1761
    /**
1762
     * convert a BTC value to a Satoshi value
1763
     *
1764
     * @param float     $btc
1765
     * @return string
1766
     */
1767 12
    public static function toSatoshi($btc) {
1768 12
        return (int)self::toSatoshiString($btc);
1769
    }
1770
1771
    /**
1772
     * json_decode helper that throws exceptions when it fails to decode
1773
     *
1774
     * @param      $json
1775
     * @param bool $assoc
1776
     * @return mixed
1777
     * @throws \Exception
1778
     */
1779 28
    protected static function jsonDecode($json, $assoc = false) {
1780 28
        if (!$json) {
1781
            throw new \Exception("Can't json_decode empty string [{$json}]");
1782
        }
1783
1784 28
        $data = json_decode($json, $assoc);
1785
1786 28
        if ($data === null) {
1787
            throw new \Exception("Failed to json_decode [{$json}]");
1788
        }
1789
1790 28
        return $data;
1791
    }
1792
1793
    /**
1794
     * sort public keys for multisig script
1795
     *
1796
     * @param PublicKeyInterface[] $pubKeys
1797
     * @return PublicKeyInterface[]
1798
     */
1799 16
    public static function sortMultisigKeys(array $pubKeys) {
1800 16
        $result = array_values($pubKeys);
1801
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1802 16
            $av = $a->getHex();
1803 16
            $bv = $b->getHex();
1804 16
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1805 16
        });
1806
1807 16
        return $result;
1808
    }
1809
1810
    /**
1811
     * read and decode the json payload from a webhook's POST request.
1812
     *
1813
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1814
     * @return mixed|null
1815
     * @throws \Exception
1816
     */
1817
    public static function getWebhookPayload($returnObject = false) {
1818
        $data = file_get_contents("php://input");
1819
        if ($data) {
1820
            return self::jsonDecode($data, !$returnObject);
1821
        } else {
1822
            return null;
1823
        }
1824
    }
1825
1826
    public static function normalizeBIP32KeyArray($keys) {
1827 22
        return Util::arrayMapWithIndex(function ($idx, $key) {
1828 22
            return [$idx, self::normalizeBIP32Key($key)];
1829 22
        }, $keys);
1830
    }
1831
1832
    /**
1833
     * @param array|BIP32Key $key
1834
     * @return BIP32Key
1835
     * @throws \Exception
1836
     */
1837 22
    public static function normalizeBIP32Key($key) {
1838 22
        if ($key instanceof BIP32Key) {
1839 10
            return $key;
1840
        }
1841
1842 22
        if (is_array($key) && count($key) === 2) {
1843 22
            $path = $key[1];
1844 22
            $hk = $key[0];
1845
1846 22
            if (!($hk instanceof HierarchicalKey)) {
1847 22
                $hk = HierarchicalKeyFactory::fromExtended($hk);
1848
            }
1849
1850 22
            return BIP32Key::create($hk, $path);
1851
        } else {
1852
            throw new \Exception("Bad Input");
1853
        }
1854
    }
1855
}
1856