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

BlocktrailSDK::formatBlocktrailKeys()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Bitcoin;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
11
use BitWasp\Bitcoin\Crypto\Random\Random;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
13
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
14
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
15
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
16
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
17
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
18
use BitWasp\Bitcoin\Network\NetworkInterface;
19
use BitWasp\Bitcoin\Transaction\TransactionFactory;
20
use BitWasp\Buffertools\Buffer;
21
use BitWasp\Buffertools\BufferInterface;
22
use Blocktrail\CryptoJSAES\CryptoJSAES;
23
use Blocktrail\SDK\Bitcoin\BIP32Key;
24
use Blocktrail\SDK\Connection\RestClient;
25
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
26
use Blocktrail\SDK\V3Crypt\Encryption;
27
use Blocktrail\SDK\V3Crypt\EncryptionMnemonic;
28
use Blocktrail\SDK\V3Crypt\KeyDerivation;
29
30
/**
31
 * Class BlocktrailSDK
32
 */
33
class BlocktrailSDK implements BlocktrailSDKInterface {
34
    /**
35
     * @var Connection\RestClient
36
     */
37
    protected $client;
38
39
    /**
40
     * @var string          currently only supporting; bitcoin
41
     */
42
    protected $network;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $testnet;
48
49
    /**
50
     * @var NetworkParams
51
     */
52
    protected $networkParams;
53
54
    /**
55
     * @param   string      $apiKey         the API_KEY to use for authentication
56
     * @param   string      $apiSecret      the API_SECRET to use for authentication
57
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
58
     * @param   bool        $testnet        testnet yes/no
59
     * @param   string      $apiVersion     the version of the API to consume
60
     * @param   null        $apiEndpoint    overwrite the endpoint used
61
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
62
     */
63 93
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
64 93
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
65
66 93
        if (is_null($apiEndpoint)) {
67 93
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
68 93
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
69
        }
70
71
        // normalize network and set bitcoinlib to the right magic-bytes
72 93
        $params = Util::normalizeNetwork($apiNetwork, $testnet);
73 93
        assert($params->isNetwork("bitcoin") || $params->isNetwork("bitcoincash"));
74
75 93
        $this->networkParams = $params;
76 93
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
77 93
    }
78
79
    /**
80
     * enable CURL debugging output
81
     *
82
     * @param   bool        $debug
83
     *
84
     * @codeCoverageIgnore
85
     */
86
    public function setCurlDebugging($debug = true) {
87
        $this->client->setCurlDebugging($debug);
88
    }
89
90
    /**
91
     * enable verbose errors
92
     *
93
     * @param   bool        $verboseErrors
94
     *
95
     * @codeCoverageIgnore
96
     */
97
    public function setVerboseErrors($verboseErrors = true) {
98
        $this->client->setVerboseErrors($verboseErrors);
99
    }
100
    
101
    /**
102
     * set cURL default option on Guzzle client
103
     * @param string    $key
104
     * @param bool      $value
105
     *
106
     * @codeCoverageIgnore
107
     */
108
    public function setCurlDefaultOption($key, $value) {
109
        $this->client->setCurlDefaultOption($key, $value);
110
    }
111
112
    /**
113
     * @return RestClient
114
     */
115 2
    public function getRestClient() {
116 2
        return $this->client;
117
    }
118
119
    /**
120
     * @return NetworkParams
121
     */
122 22
    public function getNetworkParams() {
123 22
        return $this->networkParams;
124
    }
125
126
    /**
127
     * get a single address
128
     * @param  string $address address hash
129
     * @return array           associative array containing the response
130
     */
131 1
    public function address($address) {
132 1
        $response = $this->client->get("address/{$address}");
133 1
        return self::jsonDecode($response->body(), true);
134
    }
135
136
    /**
137
     * get all transactions for an address (paginated)
138
     * @param  string  $address address hash
139
     * @param  integer $page    pagination: page number
140
     * @param  integer $limit   pagination: records per page (max 500)
141
     * @param  string  $sortDir pagination: sort direction (asc|desc)
142
     * @return array            associative array containing the response
143
     */
144 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
145
        $queryString = [
146 1
            'page' => $page,
147 1
            'limit' => $limit,
148 1
            'sort_dir' => $sortDir
149
        ];
150 1
        $response = $this->client->get("address/{$address}/transactions", $queryString);
151 1
        return self::jsonDecode($response->body(), true);
152
    }
153
154
    /**
155
     * get all unconfirmed transactions for an address (paginated)
156
     * @param  string  $address address hash
157
     * @param  integer $page    pagination: page number
158
     * @param  integer $limit   pagination: records per page (max 500)
159
     * @param  string  $sortDir pagination: sort direction (asc|desc)
160
     * @return array            associative array containing the response
161
     */
162 1
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
163
        $queryString = [
164 1
            'page' => $page,
165 1
            'limit' => $limit,
166 1
            'sort_dir' => $sortDir
167
        ];
168 1
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
169 1
        return self::jsonDecode($response->body(), true);
170
    }
171
172
    /**
173
     * get all unspent outputs for an address (paginated)
174
     * @param  string  $address address hash
175
     * @param  integer $page    pagination: page number
176
     * @param  integer $limit   pagination: records per page (max 500)
177
     * @param  string  $sortDir pagination: sort direction (asc|desc)
178
     * @return array            associative array containing the response
179
     */
180 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
181
        $queryString = [
182 1
            'page' => $page,
183 1
            'limit' => $limit,
184 1
            'sort_dir' => $sortDir
185
        ];
186 1
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
187 1
        return self::jsonDecode($response->body(), true);
188
    }
189
190
    /**
191
     * get all unspent outputs for a batch of addresses (paginated)
192
     *
193
     * @param  string[] $addresses
194
     * @param  integer  $page    pagination: page number
195
     * @param  integer  $limit   pagination: records per page (max 500)
196
     * @param  string   $sortDir pagination: sort direction (asc|desc)
197
     * @return array associative array containing the response
198
     * @throws \Exception
199
     */
200
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
201
        $queryString = [
202
            'page' => $page,
203
            'limit' => $limit,
204
            'sort_dir' => $sortDir
205
        ];
206
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
207
        return self::jsonDecode($response->body(), true);
208
    }
209
210
    /**
211
     * verify ownership of an address
212
     * @param  string  $address     address hash
213
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
214
     * @return array                associative array containing the response
215
     */
216 2
    public function verifyAddress($address, $signature) {
217 2
        $postData = ['signature' => $signature];
218
219 2
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
220
221 2
        return self::jsonDecode($response->body(), true);
222
    }
223
224
    /**
225
     * get all blocks (paginated)
226
     * @param  integer $page    pagination: page number
227
     * @param  integer $limit   pagination: records per page
228
     * @param  string  $sortDir pagination: sort direction (asc|desc)
229
     * @return array            associative array containing the response
230
     */
231 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
232
        $queryString = [
233 1
            'page' => $page,
234 1
            'limit' => $limit,
235 1
            'sort_dir' => $sortDir
236
        ];
237 1
        $response = $this->client->get("all-blocks", $queryString);
238 1
        return self::jsonDecode($response->body(), true);
239
    }
240
241
    /**
242
     * get the latest block
243
     * @return array            associative array containing the response
244
     */
245 1
    public function blockLatest() {
246 1
        $response = $this->client->get("block/latest");
247 1
        return self::jsonDecode($response->body(), true);
248
    }
249
250
    /**
251
     * get an individual block
252
     * @param  string|integer $block    a block hash or a block height
253
     * @return array                    associative array containing the response
254
     */
255 1
    public function block($block) {
256 1
        $response = $this->client->get("block/{$block}");
257 1
        return self::jsonDecode($response->body(), true);
258
    }
259
260
    /**
261
     * get all transaction in a block (paginated)
262
     * @param  string|integer   $block   a block hash or a block height
263
     * @param  integer          $page    pagination: page number
264
     * @param  integer          $limit   pagination: records per page
265
     * @param  string           $sortDir pagination: sort direction (asc|desc)
266
     * @return array                     associative array containing the response
267
     */
268 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
269
        $queryString = [
270 1
            'page' => $page,
271 1
            'limit' => $limit,
272 1
            'sort_dir' => $sortDir
273
        ];
274 1
        $response = $this->client->get("block/{$block}/transactions", $queryString);
275 1
        return self::jsonDecode($response->body(), true);
276
    }
277
278
    /**
279
     * get a single transaction
280
     * @param  string $txhash transaction hash
281
     * @return array          associative array containing the response
282
     */
283 4
    public function transaction($txhash) {
284 4
        $response = $this->client->get("transaction/{$txhash}");
285 4
        return self::jsonDecode($response->body(), true);
286
    }
287
288
    /**
289
     * get a single transaction
290
     * @param  string[] $txhashes list of transaction hashes (up to 20)
291
     * @return array[]            array containing the response
292
     */
293
    public function transactions($txhashes) {
294
        $response = $this->client->get("transactions/" . implode(",", $txhashes));
295
        return self::jsonDecode($response->body(), true);
296
    }
297
    
298
    /**
299
     * get a paginated list of all webhooks associated with the api user
300
     * @param  integer          $page    pagination: page number
301
     * @param  integer          $limit   pagination: records per page
302
     * @return array                     associative array containing the response
303
     */
304 1
    public function allWebhooks($page = 1, $limit = 20) {
305
        $queryString = [
306 1
            'page' => $page,
307 1
            'limit' => $limit
308
        ];
309 1
        $response = $this->client->get("webhooks", $queryString);
310 1
        return self::jsonDecode($response->body(), true);
311
    }
312
313
    /**
314
     * get an existing webhook by it's identifier
315
     * @param string    $identifier     a unique identifier associated with the webhook
316
     * @return array                    associative array containing the response
317
     */
318 1
    public function getWebhook($identifier) {
319 1
        $response = $this->client->get("webhook/".$identifier);
320 1
        return self::jsonDecode($response->body(), true);
321
    }
322
323
    /**
324
     * create a new webhook
325
     * @param  string  $url        the url to receive the webhook events
326
     * @param  string  $identifier a unique identifier to associate with this webhook
327
     * @return array               associative array containing the response
328
     */
329 1
    public function setupWebhook($url, $identifier = null) {
330
        $postData = [
331 1
            'url'        => $url,
332 1
            'identifier' => $identifier
333
        ];
334 1
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
335 1
        return self::jsonDecode($response->body(), true);
336
    }
337
338
    /**
339
     * update an existing webhook
340
     * @param  string  $identifier      the unique identifier of the webhook to update
341
     * @param  string  $newUrl          the new url to receive the webhook events
342
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
343
     * @return array                    associative array containing the response
344
     */
345 1
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
346
        $putData = [
347 1
            'url'        => $newUrl,
348 1
            'identifier' => $newIdentifier
349
        ];
350 1
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
351 1
        return self::jsonDecode($response->body(), true);
352
    }
353
354
    /**
355
     * deletes an existing webhook and any event subscriptions associated with it
356
     * @param  string  $identifier      the unique identifier of the webhook to delete
357
     * @return boolean                  true on success
358
     */
359 1
    public function deleteWebhook($identifier) {
360 1
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
361 1
        return self::jsonDecode($response->body(), true);
362
    }
363
364
    /**
365
     * get a paginated list of all the events a webhook is subscribed to
366
     * @param  string  $identifier  the unique identifier of the webhook
367
     * @param  integer $page        pagination: page number
368
     * @param  integer $limit       pagination: records per page
369
     * @return array                associative array containing the response
370
     */
371 2
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
372
        $queryString = [
373 2
            'page' => $page,
374 2
            'limit' => $limit
375
        ];
376 2
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
377 2
        return self::jsonDecode($response->body(), true);
378
    }
379
    
380
    /**
381
     * subscribes a webhook to transaction events of one particular transaction
382
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
383
     * @param  string  $transaction     the transaction hash
384
     * @param  integer $confirmations   the amount of confirmations to send.
385
     * @return array                    associative array containing the response
386
     */
387 1
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
388
        $postData = [
389 1
            'event_type'    => 'transaction',
390 1
            'transaction'   => $transaction,
391 1
            'confirmations' => $confirmations,
392
        ];
393 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
394 1
        return self::jsonDecode($response->body(), true);
395
    }
396
397
    /**
398
     * subscribes a webhook to transaction events on a particular address
399
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
400
     * @param  string  $address         the address hash
401
     * @param  integer $confirmations   the amount of confirmations to send.
402
     * @return array                    associative array containing the response
403
     */
404 1
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
405
        $postData = [
406 1
            'event_type'    => 'address-transactions',
407 1
            'address'       => $address,
408 1
            'confirmations' => $confirmations,
409
        ];
410 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
411 1
        return self::jsonDecode($response->body(), true);
412
    }
413
414
    /**
415
     * batch subscribes a webhook to multiple transaction events
416
     *
417
     * @param  string $identifier   the unique identifier of the webhook
418
     * @param  array  $batchData    A 2D array of event data:
419
     *                              [address => $address, confirmations => $confirmations]
420
     *                              where $address is the address to subscibe to
421
     *                              and optionally $confirmations is the amount of confirmations
422
     * @return boolean              true on success
423
     */
424 1
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
425 1
        $postData = [];
426 1
        foreach ($batchData as $record) {
427 1
            $postData[] = [
428 1
                'event_type' => 'address-transactions',
429 1
                'address' => $record['address'],
430 1
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
431
            ];
432
        }
433 1
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
434 1
        return self::jsonDecode($response->body(), true);
435
    }
436
437
    /**
438
     * subscribes a webhook to a new block event
439
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
440
     * @return array                associative array containing the response
441
     */
442 1
    public function subscribeNewBlocks($identifier) {
443
        $postData = [
444 1
            'event_type'    => 'block',
445
        ];
446 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
447 1
        return self::jsonDecode($response->body(), true);
448
    }
449
450
    /**
451
     * removes an transaction event subscription from a webhook
452
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
453
     * @param  string  $transaction     the transaction hash of the event subscription
454
     * @return boolean                  true on success
455
     */
456 1
    public function unsubscribeTransaction($identifier, $transaction) {
457 1
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
458 1
        return self::jsonDecode($response->body(), true);
459
    }
460
461
    /**
462
     * removes an address transaction event subscription from a webhook
463
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
464
     * @param  string  $address         the address hash of the event subscription
465
     * @return boolean                  true on success
466
     */
467 1
    public function unsubscribeAddressTransactions($identifier, $address) {
468 1
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
469 1
        return self::jsonDecode($response->body(), true);
470
    }
471
472
    /**
473
     * removes a block event subscription from a webhook
474
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
475
     * @return boolean                  true on success
476
     */
477 1
    public function unsubscribeNewBlocks($identifier) {
478 1
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
479 1
        return self::jsonDecode($response->body(), true);
480
    }
481
482
    /**
483
     * create a new wallet
484
     *   - will generate a new primary seed (with password) and backup seed (without password)
485
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
486
     *   - receive the blocktrail co-signing public key from the server
487
     *
488
     * Either takes one argument:
489
     * @param array $options
490
     *
491
     * Or takes three arguments (old, deprecated syntax):
492
     * (@nonPHP-doc) @param      $identifier
493
     * (@nonPHP-doc) @param      $password
494
     * (@nonPHP-doc) @param int  $keyIndex          override for the blocktrail cosigning key to use
0 ignored issues
show
Bug introduced by
There is no parameter named $keyIndex. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
495
     *
496
     * @return array[WalletInterface, array]      list($wallet, $backupInfo)
0 ignored issues
show
Documentation introduced by
The doc-type array[WalletInterface, could not be parsed: Expected "]" at position 2, but found "WalletInterface". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

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