Completed
Pull Request — master (#85)
by thomas
15:44
created

BlocktrailSDK   F

Complexity

Total Complexity 211

Size/Duplication

Total Lines 1783
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 31

Test Coverage

Coverage 85.42%

Importance

Changes 0
Metric Value
dl 0
loc 1783
ccs 586
cts 686
cp 0.8542
rs 0.5217
c 0
b 0
f 0
wmc 211
lcom 1
cbo 31

83 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
A normalizeNetwork() 0 3 1
A setBitcoinLibMagicBytes() 0 4 3
A setCurlDebugging() 0 3 1
A setVerboseErrors() 0 3 1
A setCurlDefaultOption() 0 3 1
A getRestClient() 0 3 1
A address() 0 4 1
A addressTransactions() 0 9 1
A addressUnconfirmedTransactions() 0 9 1
A addressUnspentOutputs() 0 9 1
A batchAddressUnspentOutputs() 0 9 1
A verifyAddress() 0 7 1
A allBlocks() 0 9 1
A blockLatest() 0 4 1
A block() 0 4 1
A blockTransactions() 0 9 1
A transaction() 0 4 1
A transactions() 0 4 1
A allWebhooks() 0 8 1
A getWebhook() 0 4 1
A setupWebhook() 0 8 1
A updateWebhook() 0 8 1
A deleteWebhook() 0 4 1
A getWebhookEvents() 0 8 1
A subscribeTransaction() 0 9 1
A subscribeAddressTransactions() 0 9 1
A batchSubscribeAddressTransactions() 0 12 3
A subscribeNewBlocks() 0 7 1
A unsubscribeTransaction() 0 4 1
A unsubscribeAddressTransactions() 0 4 1
A unsubscribeNewBlocks() 0 4 1
D createNewWallet() 0 44 11
F createNewWalletV1() 0 110 25
A randomBits() 0 3 1
A randomBytes() 0 3 1
F createNewWalletV2() 0 120 22
F createNewWalletV3() 0 138 25
A verifyPublicBIP32Key() 0 10 3
A verifyPublicOnly() 0 4 1
A storeNewWalletV1() 0 13 1
A storeNewWalletV2() 0 16 1
A storeNewWalletV3() 0 18 1
A upgradeKeyIndex() 0 9 1
F initWallet() 0 98 23
A getWallet() 0 4 1
A updateWallet() 0 4 1
A deleteWallet() 0 7 1
A newBackupSeed() 0 5 1
A newPrimarySeed() 0 5 1
A generateNewSeed() 0 17 3
A generateNewMnemonic() 0 10 2
A getWalletBalance() 0 4 1
A doWalletDiscovery() 0 4 1
A getNewDerivation() 0 4 1
A _getNewDerivation() 0 4 1
A getPathForAddress() 0 4 1
B sendTransaction() 0 30 6
A coinSelection() 0 20 2
A walletMaxSpendable() 0 19 2
A feePerKB() 0 4 1
A price() 0 4 1
A setupWalletWebhook() 0 4 1
A deleteWalletWebhook() 0 4 1
A lockWalletUTXO() 0 4 1
A unlockWalletUTXO() 0 4 1
A walletTransactions() 0 9 1
A walletAddresses() 0 9 1
A walletUTXOs() 0 10 1
A allWallets() 0 8 1
A sendRawTransaction() 0 4 1
A faucetWithdrawal() 0 7 1
A faucetWithdrawl() 0 3 1
A verifyMessage() 0 17 2
A toBTC() 0 3 1
A toBTCString() 0 3 1
A toSatoshiString() 0 3 1
A toSatoshi() 0 3 1
A jsonDecode() 0 13 3
A sortMultisigKeys() 0 10 3
A getWebhookPayload() 0 8 2
A normalizeBIP32KeyArray() 0 5 1
A normalizeBIP32Key() 0 18 4

How to fix   Complexity   

Complex Class

Complex classes like BlocktrailSDK often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BlocktrailSDK, and based on these observations, apply Extract Interface, too.

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 80
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
59
60 80
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
61
62 80
        if (is_null($apiEndpoint)) {
63 80
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
64 80
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
65
        }
66
67
        // normalize network and set bitcoinlib to the right magic-bytes
68 80
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
69 80
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
70
71 80
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
72 80
    }
73
74
    /**
75
     * normalize network string
76
     *
77
     * @param $network
78
     * @param $testnet
79
     * @return array
80
     * @throws \Exception
81
     */
82 80
    protected function normalizeNetwork($network, $testnet) {
83 80
        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 80
    protected function setBitcoinLibMagicBytes($network, $testnet) {
93 80
        assert($network == "bitcoin" || $network == "bitcoincash");
94 80
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
95 80
    }
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($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
633
634
        // received the blocktrail public keys
635
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
636 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
637 1
        }, $data['blocktrail_public_keys']);
638
639 1
        $wallet = new WalletV1(
640 1
            $this,
641 1
            $options['identifier'],
642 1
            $primaryMnemonic,
643 1
            [$options['key_index'] => $primaryPublicKey],
644 1
            $backupPublicKey,
645 1
            $blocktrailPublicKeys,
646 1
            $options['key_index'],
647 1
            $this->network,
648 1
            $this->testnet,
649 1
            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
        if (isset($options['primary_private_key'])) {
726 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
727
        } else {
728 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
729
        }
730
731
        // create primary public key from the created private key
732 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
733
734 2
        if (!isset($options['backup_public_key'])) {
735 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
736
        }
737
738
        // create a checksum of our private key which we'll later use to verify we used the right password
739 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
740
741
        // send the public keys and encrypted data to server
742 2
        $data = $this->storeNewWalletV2(
743 2
            $options['identifier'],
744 2
            $options['primary_public_key']->tuple(),
745 2
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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