Completed
Pull Request — master (#118)
by thomas
02:37
created

BlocktrailSDK::newV2EncryptedPrimarySeed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
10
use BitWasp\Bitcoin\Crypto\Random\Random;
11
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
13
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
14
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
15
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
16
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
17
use BitWasp\Bitcoin\Network\NetworkFactory;
18
use BitWasp\Bitcoin\Transaction\TransactionFactory;
19
use BitWasp\Buffertools\Buffer;
20
use BitWasp\Buffertools\BufferInterface;
21
use Blocktrail\CryptoJSAES\CryptoJSAES;
22
use Blocktrail\SDK\Address\AddressReaderBase;
23
use Blocktrail\SDK\Address\BitcoinAddressReader;
24
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
25
use Blocktrail\SDK\Address\CashAddress;
26
use Blocktrail\SDK\Backend\BlocktrailConverter;
27
use Blocktrail\SDK\Backend\BtccomConverter;
28
use Blocktrail\SDK\Backend\ConverterInterface;
29
use Blocktrail\SDK\Bitcoin\BIP32Key;
30
use Blocktrail\SDK\Connection\RestClient;
31
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
32
use Blocktrail\SDK\Network\BitcoinCash;
33
use Blocktrail\SDK\Connection\RestClientInterface;
34
use Blocktrail\SDK\Network\BitcoinCashRegtest;
35
use Blocktrail\SDK\Network\BitcoinCashTestnet;
36
use Btccom\JustEncrypt\Encryption;
37
use Btccom\JustEncrypt\EncryptionMnemonic;
38
use Btccom\JustEncrypt\KeyDerivation;
39
40
/**
41
 * Class BlocktrailSDK
42
 */
43
class BlocktrailSDK implements BlocktrailSDKInterface {
44
    /**
45
     * @var Connection\RestClientInterface
46
     */
47
    protected $blocktrailClient;
48
49
    /**
50
     * @var Connection\RestClient
51
     */
52
    protected $dataClient;
53
54
    /**
55
     * @var string          currently only supporting; bitcoin
56
     */
57
    protected $network;
58
59
    /**
60
     * @var bool
61
     */
62
    protected $testnet;
63
64
    /**
65
     * @var ConverterInterface
66
     */
67
    protected $converter;
68
69
    /**
70
     * @param   string      $apiKey         the API_KEY to use for authentication
71
     * @param   string      $apiSecret      the API_SECRET to use for authentication
72
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
73
     * @param   bool        $testnet        testnet yes/no
74
     * @param   string      $apiVersion     the version of the API to consume
75
     * @param   null        $apiEndpoint    overwrite the endpoint used
76
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
77
     */
78 17
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
79
80 17
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
81
82 17
        if (is_null($apiEndpoint)) {
83 17
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://wallet-api.btc.com";
84 17
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
85
        }
86
87
        // normalize network and set bitcoinlib to the right magic-bytes
88 17
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
89 17
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
90
91 17
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT');
92 17
        if (!$btccomEndpoint) {
93
            $btccomEndpoint = "https://" . ($this->network === "BCC" ? "bch-chain" : "chain") . ".api.btc.com";
94
        }
95 17
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
96
97 17
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
98 11
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
99
        }
100
101 17
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
102 17
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
103
104 17
        $this->converter = new BtccomConverter();
105 17
    }
106
107
    /**
108
     * normalize network string
109
     *
110
     * @param $network
111
     * @param $testnet
112
     * @return array
113
     * @throws \Exception
114
     */
115 17
    protected function normalizeNetwork($network, $testnet) {
116
        // [name, testnet, network]
117 17
        return Util::normalizeNetwork($network, $testnet);
118
    }
119
120
    /**
121
     * set BitcoinLib to the correct magic-byte defaults for the selected network
122
     *
123
     * @param $network
124
     * @param bool $testnet
125
     * @param bool $regtest
126
     */
127 17
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
128
129 17
        if ($network === "bitcoin") {
130 17
            if ($regtest) {
131 11
                $useNetwork = NetworkFactory::bitcoinRegtest();
132 6
            } else if ($testnet) {
133
                $useNetwork = NetworkFactory::bitcoinTestnet();
134
            } else {
135 17
                $useNetwork = NetworkFactory::bitcoin();
136
            }
137
        } else if ($network === "bitcoincash") {
138
            if ($regtest) {
139
                $useNetwork = new BitcoinCashRegtest();
140
            } else if ($testnet) {
141
                $useNetwork = new BitcoinCashTestnet();
142
            } else {
143
                $useNetwork = new BitcoinCash();
144
            }
145
        }
146
147 17
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
148 17
    }
149
150
    /**
151
     * enable CURL debugging output
152
     *
153
     * @param   bool        $debug
154
     *
155
     * @codeCoverageIgnore
156
     */
157
    public function setCurlDebugging($debug = true) {
158
        $this->blocktrailClient->setCurlDebugging($debug);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDebugging() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
159
        $this->dataClient->setCurlDebugging($debug);
160
    }
161
162
    /**
163
     * enable verbose errors
164
     *
165
     * @param   bool        $verboseErrors
166
     *
167
     * @codeCoverageIgnore
168
     */
169
    public function setVerboseErrors($verboseErrors = true) {
170
        $this->blocktrailClient->setVerboseErrors($verboseErrors);
171
        $this->dataClient->setVerboseErrors($verboseErrors);
172
    }
173
    
174
    /**
175
     * set cURL default option on Guzzle client
176
     * @param string    $key
177
     * @param bool      $value
178
     *
179
     * @codeCoverageIgnore
180
     */
181
    public function setCurlDefaultOption($key, $value) {
182
        $this->blocktrailClient->setCurlDefaultOption($key, $value);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDefaultOption() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
183
        $this->dataClient->setCurlDefaultOption($key, $value);
184
    }
185
186
    /**
187
     * @return  RestClientInterface
188
     */
189 1
    public function getRestClient() {
190 1
        return $this->blocktrailClient;
191
    }
192
193
    /**
194
     * @return  RestClient
195
     */
196
    public function getDataRestClient() {
197
        return $this->dataClient;
198
    }
199
200
    /**
201
     * @param RestClientInterface $restClient
202
     */
203
    public function setRestClient(RestClientInterface $restClient) {
204
        $this->blocktrailClient = $restClient;
205
    }
206
207
    /**
208
     * get a single address
209
     * @param  string $address address hash
210
     * @return array           associative array containing the response
211
     */
212 2
    public function address($address) {
213 2
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
214 2
        return $this->converter->convertAddress($response->body());
215
    }
216
217
    /**
218
     * get all transactions for an address (paginated)
219
     * @param  string  $address address hash
220
     * @param  integer $page    pagination: page number
221
     * @param  integer $limit   pagination: records per page (max 500)
222
     * @param  string  $sortDir pagination: sort direction (asc|desc)
223
     * @return array            associative array containing the response
224
     */
225 2
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
226
        $queryString = [
227 2
            'page' => $page,
228 2
            'limit' => $limit,
229 2
            'sort_dir' => $sortDir,
230
        ];
231 2
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
232 2
        return $this->converter->convertAddressTxs($response->body());
233
    }
234
235
    /**
236
     * get all unconfirmed transactions for an address (paginated)
237
     * @param  string  $address address hash
238
     * @param  integer $page    pagination: page number
239
     * @param  integer $limit   pagination: records per page (max 500)
240
     * @param  string  $sortDir pagination: sort direction (asc|desc)
241
     * @return array            associative array containing the response
242
     */
243
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
244
        $queryString = [
245
            'page' => $page,
246
            'limit' => $limit,
247
            'sort_dir' => $sortDir
248
        ];
249
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
250
        return $this->converter->convertAddressTxs($response->body());
251
    }
252
253
    /**
254
     * get all unspent outputs for an address (paginated)
255
     * @param  string  $address address hash
256
     * @param  integer $page    pagination: page number
257
     * @param  integer $limit   pagination: records per page (max 500)
258
     * @param  string  $sortDir pagination: sort direction (asc|desc)
259
     * @return array            associative array containing the response
260
     */
261 2
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
262
        $queryString = [
263 2
            'page' => $page,
264 2
            'limit' => $limit,
265 2
            'sort_dir' => $sortDir
266
        ];
267 2
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
268 2
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
269
    }
270
271
    /**
272
     * get all unspent outputs for a batch of addresses (paginated)
273
     *
274
     * @param  string[] $addresses
275
     * @param  integer  $page    pagination: page number
276
     * @param  integer  $limit   pagination: records per page (max 500)
277
     * @param  string   $sortDir pagination: sort direction (asc|desc)
278
     * @return array associative array containing the response
279
     * @throws \Exception
280
     */
281
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
282
        $queryString = [
283
            'page' => $page,
284
            'limit' => $limit,
285
            'sort_dir' => $sortDir
286
        ];
287
288
        if ($this->converter instanceof BtccomConverter) {
289
            if ($page > 1) {
290
                return [
291
                    'data' => [],
292
                    'current_page' => 2,
293
                    'per_page' => null,
294
                    'total' => null,
295
                ];
296
            }
297
298
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
299
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
300
        } else {
301
            $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
0 ignored issues
show
Bug introduced by
The property client does not seem to exist. Did you mean blocktrailClient?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
302
            return self::jsonDecode($response->body(), true);
303
        }
304
    }
305
306
    /**
307
     * verify ownership of an address
308
     * @param  string  $address     address hash
309
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
310
     * @return array                associative array containing the response
311
     */
312 2
    public function verifyAddress($address, $signature) {
313 2
        if ($this->verifyMessage($address, $address, $signature)) {
314 2
            return ['result' => true, 'msg' => 'Successfully verified'];
315
        } else {
316
            return ['result' => false];
317
        }
318
    }
319
320
    /**
321
     * get all blocks (paginated)
322
     * @param  integer $page    pagination: page number
323
     * @param  integer $limit   pagination: records per page
324
     * @param  string  $sortDir pagination: sort direction (asc|desc)
325
     * @return array            associative array containing the response
326
     */
327 2
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
328
        $queryString = [
329 2
            'page' => $page,
330 2
            'limit' => $limit,
331 2
            'sort_dir' => $sortDir
332
        ];
333 2
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
334 2
        return $this->converter->convertBlocks($response->body());
335
    }
336
337
    /**
338
     * get the latest block
339
     * @return array            associative array containing the response
340
     */
341 1
    public function blockLatest() {
342 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
343 1
        return $this->converter->convertBlock($response->body());
344
    }
345
346
    /**
347
     * get an individual block
348
     * @param  string|integer $block    a block hash or a block height
349
     * @return array                    associative array containing the response
350
     */
351 4
    public function block($block) {
352 4
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
353 4
        return $this->converter->convertBlock($response->body());
354
    }
355
356
    /**
357
     * get all transaction in a block (paginated)
358
     * @param  string|integer   $block   a block hash or a block height
359
     * @param  integer          $page    pagination: page number
360
     * @param  integer          $limit   pagination: records per page
361
     * @param  string           $sortDir pagination: sort direction (asc|desc)
362
     * @return array                     associative array containing the response
363
     */
364 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
365
        $queryString = [
366 1
            'page' => $page,
367 1
            'limit' => $limit,
368 1
            'sort_dir' => $sortDir
369
        ];
370 1
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
371 1
        return $this->converter->convertBlockTxs($response->body());
372
    }
373
374
    /**
375
     * get a single transaction
376
     * @param  string $txhash transaction hash
377
     * @return array          associative array containing the response
378
     */
379 2
    public function transaction($txhash) {
380 2
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
381 2
        $res = $this->converter->convertTx($response->body(), null);
382
383 2
        if ($this->converter instanceof BtccomConverter) {
384 1
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
385
        }
386
387 2
        return $res;
388
    }
389
390
    /**
391
     * get a single transaction
392
     * @param  string[] $txhashes list of transaction hashes (up to 20)
393
     * @return array[]            array containing the response
394
     */
395 2
    public function transactions($txhashes) {
396 2
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
397 2
        return $this->converter->convertTxs($response->body());
398
    }
399
    
400
    /**
401
     * get a paginated list of all webhooks associated with the api user
402
     * @param  integer          $page    pagination: page number
403
     * @param  integer          $limit   pagination: records per page
404
     * @return array                     associative array containing the response
405
     */
406
    public function allWebhooks($page = 1, $limit = 20) {
407
        $queryString = [
408
            'page' => $page,
409
            'limit' => $limit
410
        ];
411
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
412
        return self::jsonDecode($response->body(), true);
413
    }
414
415
    /**
416
     * get an existing webhook by it's identifier
417
     * @param string    $identifier     a unique identifier associated with the webhook
418
     * @return array                    associative array containing the response
419
     */
420
    public function getWebhook($identifier) {
421
        $response = $this->blocktrailClient->get("webhook/".$identifier);
422
        return self::jsonDecode($response->body(), true);
423
    }
424
425
    /**
426
     * create a new webhook
427
     * @param  string  $url        the url to receive the webhook events
428
     * @param  string  $identifier a unique identifier to associate with this webhook
429
     * @return array               associative array containing the response
430
     */
431
    public function setupWebhook($url, $identifier = null) {
432
        $postData = [
433
            'url'        => $url,
434
            'identifier' => $identifier
435
        ];
436
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
437
        return self::jsonDecode($response->body(), true);
438
    }
439
440
    /**
441
     * update an existing webhook
442
     * @param  string  $identifier      the unique identifier of the webhook to update
443
     * @param  string  $newUrl          the new url to receive the webhook events
444
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
445
     * @return array                    associative array containing the response
446
     */
447
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
448
        $putData = [
449
            'url'        => $newUrl,
450
            'identifier' => $newIdentifier
451
        ];
452
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
453
        return self::jsonDecode($response->body(), true);
454
    }
455
456
    /**
457
     * deletes an existing webhook and any event subscriptions associated with it
458
     * @param  string  $identifier      the unique identifier of the webhook to delete
459
     * @return boolean                  true on success
460
     */
461
    public function deleteWebhook($identifier) {
462
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
463
        return self::jsonDecode($response->body(), true);
464
    }
465
466
    /**
467
     * get a paginated list of all the events a webhook is subscribed to
468
     * @param  string  $identifier  the unique identifier of the webhook
469
     * @param  integer $page        pagination: page number
470
     * @param  integer $limit       pagination: records per page
471
     * @return array                associative array containing the response
472
     */
473
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
474
        $queryString = [
475
            'page' => $page,
476
            'limit' => $limit
477
        ];
478
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
479
        return self::jsonDecode($response->body(), true);
480
    }
481
    
482
    /**
483
     * subscribes a webhook to transaction events of one particular transaction
484
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
485
     * @param  string  $transaction     the transaction hash
486
     * @param  integer $confirmations   the amount of confirmations to send.
487
     * @return array                    associative array containing the response
488
     */
489
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
490
        $postData = [
491
            'event_type'    => 'transaction',
492
            'transaction'   => $transaction,
493
            'confirmations' => $confirmations,
494
        ];
495
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
496
        return self::jsonDecode($response->body(), true);
497
    }
498
499
    /**
500
     * subscribes a webhook to transaction events on a particular address
501
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
502
     * @param  string  $address         the address hash
503
     * @param  integer $confirmations   the amount of confirmations to send.
504
     * @return array                    associative array containing the response
505
     */
506
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
507
        $postData = [
508
            'event_type'    => 'address-transactions',
509
            'address'       => $address,
510
            'confirmations' => $confirmations,
511
        ];
512
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
513
        return self::jsonDecode($response->body(), true);
514
    }
515
516
    /**
517
     * batch subscribes a webhook to multiple transaction events
518
     *
519
     * @param  string $identifier   the unique identifier of the webhook
520
     * @param  array  $batchData    A 2D array of event data:
521
     *                              [address => $address, confirmations => $confirmations]
522
     *                              where $address is the address to subscibe to
523
     *                              and optionally $confirmations is the amount of confirmations
524
     * @return boolean              true on success
525
     */
526
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
527
        $postData = [];
528
        foreach ($batchData as $record) {
529
            $postData[] = [
530
                'event_type' => 'address-transactions',
531
                'address' => $record['address'],
532
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
533
            ];
534
        }
535
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
536
        return self::jsonDecode($response->body(), true);
537
    }
538
539
    /**
540
     * subscribes a webhook to a new block event
541
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
542
     * @return array                associative array containing the response
543
     */
544
    public function subscribeNewBlocks($identifier) {
545
        $postData = [
546
            'event_type'    => 'block',
547
        ];
548
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
549
        return self::jsonDecode($response->body(), true);
550
    }
551
552
    /**
553
     * removes an transaction event subscription from a webhook
554
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
555
     * @param  string  $transaction     the transaction hash of the event subscription
556
     * @return boolean                  true on success
557
     */
558
    public function unsubscribeTransaction($identifier, $transaction) {
559
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
560
        return self::jsonDecode($response->body(), true);
561
    }
562
563
    /**
564
     * removes an address transaction event subscription from a webhook
565
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
566
     * @param  string  $address         the address hash of the event subscription
567
     * @return boolean                  true on success
568
     */
569
    public function unsubscribeAddressTransactions($identifier, $address) {
570
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
571
        return self::jsonDecode($response->body(), true);
572
    }
573
574
    /**
575
     * removes a block event subscription from a webhook
576
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
577
     * @return boolean                  true on success
578
     */
579
    public function unsubscribeNewBlocks($identifier) {
580
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
581
        return self::jsonDecode($response->body(), true);
582
    }
583
584
    /**
585
     * create a new wallet
586
     *   - will generate a new primary seed (with password) and backup seed (without password)
587
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
588
     *   - receive the blocktrail co-signing public key from the server
589
     *
590
     * Either takes one argument:
591
     * @param array $options
592
     *
593
     * Or takes three arguments (old, deprecated syntax):
594
     * (@nonPHP-doc) @param      $identifier
595
     * (@nonPHP-doc) @param      $password
596
     * (@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...
597
     *
598
     * @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...
599
     * @throws \Exception
600
     */
601
    public function createNewWallet($options) {
602
        if (!is_array($options)) {
603
            $args = func_get_args();
604
            $options = [
605
                "identifier" => $args[0],
606
                "password" => $args[1],
607
                "key_index" => isset($args[2]) ? $args[2] : null,
608
            ];
609
        }
610
611
        if (isset($options['password'])) {
612
            if (isset($options['passphrase'])) {
613
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
614
            } else {
615
                $options['passphrase'] = $options['password'];
616
            }
617
        }
618
619
        if (!isset($options['passphrase'])) {
620
            $options['passphrase'] = null;
621
        }
622
623
        if (!isset($options['key_index'])) {
624
            $options['key_index'] = 0;
625
        }
626
627
        if (!isset($options['wallet_version'])) {
628
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
629
        }
630
631
        switch ($options['wallet_version']) {
632
            case Wallet::WALLET_VERSION_V1:
633
                return $this->createNewWalletV1($options);
634
635
            case Wallet::WALLET_VERSION_V2:
636
                return $this->createNewWalletV2($options);
637
638
            case Wallet::WALLET_VERSION_V3:
639
                return $this->createNewWalletV3($options);
640
641
            default:
642
                throw new \InvalidArgumentException("Invalid wallet version");
643
        }
644
    }
645
646
    protected function createNewWalletV1($options) {
647
        $walletPath = WalletPath::create($options['key_index']);
648
649
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
650
651
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
652
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
653
        }
654
655
        $primaryMnemonic = null;
656
        $primaryPrivateKey = null;
657
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
658
            if (!$options['passphrase']) {
659
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
660
            } else {
661
                // create new primary seed
662
                /** @var HierarchicalKey $primaryPrivateKey */
663
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
664
                if ($storePrimaryMnemonic !== false) {
665
                    $storePrimaryMnemonic = true;
666
                }
667
            }
668
        } elseif (isset($options['primary_mnemonic'])) {
669
            $primaryMnemonic = $options['primary_mnemonic'];
670
        } elseif (isset($options['primary_private_key'])) {
671
            $primaryPrivateKey = $options['primary_private_key'];
672
        }
673
674
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
675
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
676
        }
677
678
        if ($primaryPrivateKey) {
679
            if (is_string($primaryPrivateKey)) {
680
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
681
            }
682
        } else {
683
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
684
        }
685
686
        if (!$storePrimaryMnemonic) {
687
            $primaryMnemonic = false;
688
        }
689
690
        // create primary public key from the created private key
691
        $path = $walletPath->keyIndexPath()->publicPath();
692
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
693
694
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
695
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
696
        }
697
698
        $backupMnemonic = null;
699
        $backupPublicKey = null;
700
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
701
            /** @var HierarchicalKey $backupPrivateKey */
702
            list($backupMnemonic, , ) = $this->newV1BackupSeed();
703
        } else if (isset($options['backup_mnemonic'])) {
704
            $backupMnemonic = $options['backup_mnemonic'];
705
        } elseif (isset($options['backup_public_key'])) {
706
            $backupPublicKey = $options['backup_public_key'];
707
        }
708
709
        if ($backupPublicKey) {
710
            if (is_string($backupPublicKey)) {
711
                $backupPublicKey = [$backupPublicKey, "m"];
712
            }
713
        } else {
714
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
715
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
716
        }
717
718
        // create a checksum of our private key which we'll later use to verify we used the right password
719
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
720
        $addressReader = $this->makeAddressReader($options);
721
722
        // send the public keys to the server to store them
723
        //  and the mnemonic, which is safe because it's useless without the password
724
        $data = $this->storeNewWalletV1(
725
            $options['identifier'],
726
            $primaryPublicKey->tuple(),
727
            $backupPublicKey->tuple(),
728
            $primaryMnemonic,
729
            $checksum,
730
            $options['key_index'],
731
            array_key_exists('segwit', $options) ? $options['segwit'] : false
732
        );
733
734
        // received the blocktrail public keys
735
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
736
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
737
        }, $data['blocktrail_public_keys']);
738
739
        $wallet = new WalletV1(
740
            $this,
741
            $options['identifier'],
742
            $primaryMnemonic,
743
            [$options['key_index'] => $primaryPublicKey],
744
            $backupPublicKey,
745
            $blocktrailPublicKeys,
746
            $options['key_index'],
747
            $this->network,
748
            $this->testnet,
749
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
750
            $addressReader,
751
            $checksum
752
        );
753
754
        $wallet->unlock($options);
755
756
        // return wallet and backup mnemonic
757
        return [
758
            $wallet,
759
            [
760
                'primary_mnemonic' => $primaryMnemonic,
761
                'backup_mnemonic' => $backupMnemonic,
762
                'blocktrail_public_keys' => $blocktrailPublicKeys,
763
            ],
764
        ];
765
    }
766
767
    public function randomBits($bits) {
768
        return $this->randomBytes($bits / 8);
769
    }
770
771
    public function randomBytes($bytes) {
772
        return (new Random())->bytes($bytes)->getBinary();
773
    }
774
775
    protected function createNewWalletV2($options) {
776
        $walletPath = WalletPath::create($options['key_index']);
777
778
        if (isset($options['store_primary_mnemonic'])) {
779
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
780
        }
781
782
        if (!isset($options['store_data_on_server'])) {
783
            if (isset($options['primary_private_key'])) {
784
                $options['store_data_on_server'] = false;
785
            } else {
786
                $options['store_data_on_server'] = true;
787
            }
788
        }
789
790
        $storeDataOnServer = $options['store_data_on_server'];
791
792
        $secret = null;
793
        $encryptedSecret = null;
794
        $primarySeed = null;
795
        $encryptedPrimarySeed = null;
796
        $recoverySecret = null;
797
        $recoveryEncryptedSecret = null;
798
        $backupSeed = null;
799
800
        if (!isset($options['primary_private_key'])) {
801
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed();
802
        }
803
804
        if ($storeDataOnServer) {
805
            if (!isset($options['secret'])) {
806
                if (!$options['passphrase']) {
807
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
808
                }
809
810
                list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']);
811
            } else {
812
                $secret = $options['secret'];
813
            }
814
815
            $encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret);
816
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret);
817
        }
818
819
        if (!isset($options['backup_public_key'])) {
820
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed();
821
        }
822
823
        if (isset($options['primary_private_key'])) {
824
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
825
        } else {
826
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
827
        }
828
829
        // create primary public key from the created private key
830
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
831
832
        if (!isset($options['backup_public_key'])) {
833
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
834
        }
835
836
        // create a checksum of our private key which we'll later use to verify we used the right password
837
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
838
        $addressReader = $this->makeAddressReader($options);
839
840
        // send the public keys and encrypted data to server
841
        $data = $this->storeNewWalletV2(
842
            $options['identifier'],
843
            $options['primary_public_key']->tuple(),
844
            $options['backup_public_key']->tuple(),
845
            $storeDataOnServer ? $encryptedPrimarySeed : false,
846
            $storeDataOnServer ? $encryptedSecret : false,
847
            $storeDataOnServer ? $recoverySecret : false,
848
            $checksum,
849
            $options['key_index'],
850
            array_key_exists('segwit', $options) ? $options['segwit'] : false
851
        );
852
853
        // received the blocktrail public keys
854
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
855
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
856
        }, $data['blocktrail_public_keys']);
857
858
        $wallet = new WalletV2(
859
            $this,
860
            $options['identifier'],
861
            $encryptedPrimarySeed,
862
            $encryptedSecret,
863
            [$options['key_index'] => $options['primary_public_key']],
864
            $options['backup_public_key'],
865
            $blocktrailPublicKeys,
866
            $options['key_index'],
867
            $this->network,
868
            $this->testnet,
869
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
870
            $addressReader,
871
            $checksum
872
        );
873
874
        $wallet->unlock([
875
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
876
            'primary_private_key' => $options['primary_private_key'],
877
            'primary_seed' => $primarySeed,
878
            'secret' => $secret,
879
        ]);
880
881
        // return wallet and mnemonics for backup sheet
882
        return [
883
            $wallet,
884
            [
885
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
886
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
887
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
888
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
889
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
890
                    return [$keyIndex, $pubKey->tuple()];
891
                }, $blocktrailPublicKeys),
892
            ],
893
        ];
894
    }
895
896
    protected function createNewWalletV3($options) {
897
        $walletPath = WalletPath::create($options['key_index']);
898
899
        if (isset($options['store_primary_mnemonic'])) {
900
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
901
        }
902
903
        if (!isset($options['store_data_on_server'])) {
904
            if (isset($options['primary_private_key'])) {
905
                $options['store_data_on_server'] = false;
906
            } else {
907
                $options['store_data_on_server'] = true;
908
            }
909
        }
910
911
        $storeDataOnServer = $options['store_data_on_server'];
912
913
        $secret = null;
914
        $encryptedSecret = null;
915
        $primarySeed = null;
916
        $encryptedPrimarySeed = null;
917
        $recoverySecret = null;
918
        $recoveryEncryptedSecret = null;
919
        $backupSeed = null;
920
921
        if (!isset($options['primary_private_key'])) {
922
            if (isset($options['primary_seed'])) {
923
                if (!$options['primary_seed'] instanceof BufferInterface) {
924
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
925
                }
926
                $primarySeed = $options['primary_seed'];
927
            } else {
928
                $primarySeed = $this->newV3PrimarySeed();
929
            }
930
        }
931
932
        if ($storeDataOnServer) {
933
            if (!isset($options['secret'])) {
934
                if (!$options['passphrase']) {
935
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
936
                }
937
938
                list($secret, $encryptedSecret) = $this->newV3Secret($options['passphrase']);
939
            } else {
940
                if (!$options['secret'] instanceof Buffer) {
941
                    throw new \InvalidArgumentException('Secret must be provided as a Buffer');
942
                }
943
944
                $secret = $options['secret'];
945
            }
946
947
            $encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret);
948
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret);
949
        }
950
951
        if (!isset($options['backup_public_key'])) {
952
            if (isset($options['backup_seed'])) {
953
                if (!$options['backup_seed'] instanceof Buffer) {
954
                    throw new \InvalidArgumentException('Backup seed must be an instance of Buffer');
955
                }
956
                $backupSeed = $options['backup_seed'];
957
            } else {
958
                $backupSeed = $this->newV3BackupSeed();
959
            }
960
        }
961
962
        if (isset($options['primary_private_key'])) {
963
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
964
        } else {
965
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
966
        }
967
968
        // create primary public key from the created private key
969
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
970
971
        if (!isset($options['backup_public_key'])) {
972
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
973
        }
974
975
        // create a checksum of our private key which we'll later use to verify we used the right password
976
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
977
        $addressReader = $this->makeAddressReader($options);
978
979
        // send the public keys and encrypted data to server
980
        $data = $this->storeNewWalletV3(
981
            $options['identifier'],
982
            $options['primary_public_key']->tuple(),
983
            $options['backup_public_key']->tuple(),
984
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
985
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
986
            $storeDataOnServer ? $recoverySecret->getHex() : false,
987
            $checksum,
988
            $options['key_index'],
989
            array_key_exists('segwit', $options) ? $options['segwit'] : false
990
        );
991
992
        // received the blocktrail public keys
993
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
994
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
995
        }, $data['blocktrail_public_keys']);
996
997
        $wallet = new WalletV3(
998
            $this,
999
            $options['identifier'],
1000
            $encryptedPrimarySeed,
1001
            $encryptedSecret,
1002
            [$options['key_index'] => $options['primary_public_key']],
1003
            $options['backup_public_key'],
1004
            $blocktrailPublicKeys,
1005
            $options['key_index'],
1006
            $this->network,
1007
            $this->testnet,
1008
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1009
            $addressReader,
1010
            $checksum
1011
        );
1012
1013
        $wallet->unlock([
1014
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1015
            'primary_private_key' => $options['primary_private_key'],
1016
            'primary_seed' => $primarySeed,
1017
            'secret' => $secret,
1018
        ]);
1019
1020
        // return wallet and mnemonics for backup sheet
1021
        return [
1022
            $wallet,
1023
            [
1024
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1025
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1026
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1027
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1028
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1029
                    return [$keyIndex, $pubKey->tuple()];
1030
                }, $blocktrailPublicKeys),
1031
            ]
1032
        ];
1033
    }
1034
1035
    public function newV2PrimarySeed() {
1036
        return $this->randomBits(256);
1037
    }
1038
1039
    public function newV2BackupSeed() {
1040
        return $this->randomBits(256);
1041
    }
1042
1043
    public function newV2Secret($passphrase) {
1044
        $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
1045
        $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase);
1046
1047
        return [$secret, $encryptedSecret];
1048
    }
1049
1050
    public function newV2EncryptedPrimarySeed($primarySeed, $secret) {
1051
        return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
1052
    }
1053
1054
    public function newV2RecoverySecret($secret) {
1055
        $recoverySecret = bin2hex($this->randomBits(256));
1056
        $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
1057
1058
        return [$recoverySecret, $recoveryEncryptedSecret];
1059
    }
1060
1061
    public function newV3PrimarySeed() {
1062
        return new Buffer($this->randomBits(256));
1063
    }
1064
1065
    public function newV3BackupSeed() {
1066
        return new Buffer($this->randomBits(256));
1067
    }
1068
1069
    public function newV3Secret($passphrase) {
1070
        $secret = new Buffer($this->randomBits(256));
1071
        $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
1072
            ->getBuffer();
1073
1074
        return [$secret, $encryptedSecret];
1075
    }
1076
1077
    public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
1078
        return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
1079
            ->getBuffer();
1080
    }
1081
1082
    public function newV3RecoverySecret(Buffer $secret) {
1083
        $recoverySecret = new Buffer($this->randomBits(256));
1084
        $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
1085
            ->getBuffer();
1086
1087
        return [$recoverySecret, $recoveryEncryptedSecret];
1088
    }
1089
1090
    /**
1091
     * @param array $bip32Key
1092
     * @throws BlocktrailSDKException
1093
     */
1094
    private function verifyPublicBIP32Key(array $bip32Key) {
1095
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1096
        if ($hk->isPrivate()) {
1097
            throw new BlocktrailSDKException('Private key was included in request, abort');
1098
        }
1099
1100
        if (substr($bip32Key[1], 0, 1) === "m") {
1101
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1102
        }
1103
    }
1104
1105
    /**
1106
     * @param array $walletData
1107
     * @throws BlocktrailSDKException
1108
     */
1109
    private function verifyPublicOnly(array $walletData) {
1110
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1111
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1112
    }
1113
1114
    /**
1115
     * create wallet using the API
1116
     *
1117
     * @param string    $identifier             the wallet identifier to create
1118
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1119
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1120
     * @param string    $primaryMnemonic        mnemonic to store
1121
     * @param string    $checksum               checksum to store
1122
     * @param int       $keyIndex               account that we expect to use
1123
     * @param bool      $segwit                 opt in to segwit
1124
     * @return mixed
1125
     */
1126
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1127
        $data = [
1128
            'identifier' => $identifier,
1129
            'primary_public_key' => $primaryPublicKey,
1130
            'backup_public_key' => $backupPublicKey,
1131
            'primary_mnemonic' => $primaryMnemonic,
1132
            'checksum' => $checksum,
1133
            'key_index' => $keyIndex,
1134
            'segwit' => $segwit,
1135
        ];
1136
        $this->verifyPublicOnly($data);
1137
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1138
        return self::jsonDecode($response->body(), true);
1139
    }
1140
1141
    /**
1142
     * create wallet using the API
1143
     *
1144
     * @param string $identifier       the wallet identifier to create
1145
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1146
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1147
     * @param        $encryptedPrimarySeed
1148
     * @param        $encryptedSecret
1149
     * @param        $recoverySecret
1150
     * @param string $checksum         checksum to store
1151
     * @param int    $keyIndex         account that we expect to use
1152
     * @param bool   $segwit           opt in to segwit
1153
     * @return mixed
1154
     * @throws \Exception
1155
     */
1156
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1157
        $data = [
1158
            'identifier' => $identifier,
1159
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1160
            'primary_public_key' => $primaryPublicKey,
1161
            'backup_public_key' => $backupPublicKey,
1162
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1163
            'encrypted_secret' => $encryptedSecret,
1164
            'recovery_secret' => $recoverySecret,
1165
            'checksum' => $checksum,
1166
            'key_index' => $keyIndex,
1167
            'segwit' => $segwit,
1168
        ];
1169
        $this->verifyPublicOnly($data);
1170
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1171
        return self::jsonDecode($response->body(), true);
1172
    }
1173
1174
    /**
1175
     * create wallet using the API
1176
     *
1177
     * @param string $identifier       the wallet identifier to create
1178
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1179
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1180
     * @param        $encryptedPrimarySeed
1181
     * @param        $encryptedSecret
1182
     * @param        $recoverySecret
1183
     * @param string $checksum         checksum to store
1184
     * @param int    $keyIndex         account that we expect to use
1185
     * @param bool   $segwit           opt in to segwit
1186
     * @return mixed
1187
     * @throws \Exception
1188
     */
1189
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1190
1191
        $data = [
1192
            'identifier' => $identifier,
1193
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1194
            'primary_public_key' => $primaryPublicKey,
1195
            'backup_public_key' => $backupPublicKey,
1196
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1197
            'encrypted_secret' => $encryptedSecret,
1198
            'recovery_secret' => $recoverySecret,
1199
            'checksum' => $checksum,
1200
            'key_index' => $keyIndex,
1201
            'segwit' => $segwit,
1202
        ];
1203
1204
        $this->verifyPublicOnly($data);
1205
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1206
        return self::jsonDecode($response->body(), true);
1207
    }
1208
1209
    /**
1210
     * upgrade wallet to use a new account number
1211
     *  the account number specifies which blocktrail cosigning key is used
1212
     *
1213
     * @param string    $identifier             the wallet identifier to be upgraded
1214
     * @param int       $keyIndex               the new account to use
1215
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1216
     * @return mixed
1217
     */
1218
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1219
        $data = [
1220
            'key_index' => $keyIndex,
1221
            'primary_public_key' => $primaryPublicKey
1222
        ];
1223
1224
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1225
        return self::jsonDecode($response->body(), true);
1226
    }
1227
1228
    /**
1229
     * @param array $options
1230
     * @return AddressReaderBase
1231
     */
1232
    private function makeAddressReader(array $options) {
1233
        if ($this->network == "bitcoincash") {
1234
            $useCashAddress = false;
1235
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1236
                $useCashAddress = true;
1237
            }
1238
            return new BitcoinCashAddressReader($useCashAddress);
1239
        } else {
1240
            return new BitcoinAddressReader();
1241
        }
1242
    }
1243
1244
    /**
1245
     * initialize a previously created wallet
1246
     *
1247
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1248
     *
1249
     * Some of the options:
1250
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1251
     *    so the wallet is loaded in read-only mode (no private key)
1252
     *  - "check_backup_key" can be set to your own backup key:
1253
     *    Format: ["M', "xpub..."]
1254
     *    Setting this will allow the SDK to check the server hasn't
1255
     *    a different key (one it happens to control)
1256
1257
     * Either takes one argument:
1258
     * @param array $options
1259
     *
1260
     * Or takes two arguments (old, deprecated syntax):
1261
     * (@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...
1262
     * (@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...
1263
     *
1264
     * @return WalletInterface
1265
     * @throws \Exception
1266
     */
1267
    public function initWallet($options) {
1268
        if (!is_array($options)) {
1269
            $args = func_get_args();
1270
            $options = [
1271
                "identifier" => $args[0],
1272
                "password" => $args[1],
1273
            ];
1274
        }
1275
1276
        $identifier = $options['identifier'];
1277
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1278
                    (isset($options['readOnly']) ? $options['readOnly'] :
1279
                        (isset($options['read-only']) ? $options['read-only'] :
1280
                            false));
1281
1282
        // get the wallet data from the server
1283
        $data = $this->getWallet($identifier);
1284
        if (!$data) {
1285
            throw new \Exception("Failed to get wallet");
1286
        }
1287
1288
        if (array_key_exists('check_backup_key', $options)) {
1289
            if (!is_string($options['check_backup_key'])) {
1290
                throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
1291
            }
1292
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1293
                throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
1294
            }
1295
        }
1296
1297
        $addressReader = $this->makeAddressReader($options);
1298
1299
        switch ($data['wallet_version']) {
1300
            case Wallet::WALLET_VERSION_V1:
1301
                $wallet = new WalletV1(
1302
                    $this,
1303
                    $identifier,
1304
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1305
                    $data['primary_public_keys'],
1306
                    $data['backup_public_key'],
1307
                    $data['blocktrail_public_keys'],
1308
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1309
                    $this->network,
1310
                    $this->testnet,
1311
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1312
                    $addressReader,
1313
                    $data['checksum']
1314
                );
1315
                break;
1316
            case Wallet::WALLET_VERSION_V2:
1317
                $wallet = new WalletV2(
1318
                    $this,
1319
                    $identifier,
1320
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1321
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1322
                    $data['primary_public_keys'],
1323
                    $data['backup_public_key'],
1324
                    $data['blocktrail_public_keys'],
1325
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1326
                    $this->network,
1327
                    $this->testnet,
1328
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1329
                    $addressReader,
1330
                    $data['checksum']
1331
                );
1332
                break;
1333
            case Wallet::WALLET_VERSION_V3:
1334
                if (isset($options['encrypted_primary_seed'])) {
1335
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1336
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1337
                    }
1338
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1339
                } else {
1340
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1341
                }
1342
1343
                if (isset($options['encrypted_secret'])) {
1344
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1345
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1346
                    }
1347
1348
                    $encryptedSecret = $data['encrypted_secret'];
1349
                } else {
1350
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1351
                }
1352
1353
                $wallet = new WalletV3(
1354
                    $this,
1355
                    $identifier,
1356
                    $encryptedPrimarySeed,
1357
                    $encryptedSecret,
1358
                    $data['primary_public_keys'],
1359
                    $data['backup_public_key'],
1360
                    $data['blocktrail_public_keys'],
1361
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1362
                    $this->network,
1363
                    $this->testnet,
1364
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1365
                    $addressReader,
1366
                    $data['checksum']
1367
                );
1368
                break;
1369
            default:
1370
                throw new \InvalidArgumentException("Invalid wallet version");
1371
        }
1372
1373
        if (!$readonly) {
1374
            $wallet->unlock($options);
1375
        }
1376
1377
        return $wallet;
1378
    }
1379
1380
    /**
1381
     * get the wallet data from the server
1382
     *
1383
     * @param string    $identifier             the identifier of the wallet
1384
     * @return mixed
1385
     */
1386
    public function getWallet($identifier) {
1387
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1388
        return self::jsonDecode($response->body(), true);
1389
    }
1390
1391
    /**
1392
     * update the wallet data on the server
1393
     *
1394
     * @param string    $identifier
1395
     * @param $data
1396
     * @return mixed
1397
     */
1398
    public function updateWallet($identifier, $data) {
1399
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1400
        return self::jsonDecode($response->body(), true);
1401
    }
1402
1403
    /**
1404
     * delete a wallet from the server
1405
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1406
     *  is required to be able to delete a wallet
1407
     *
1408
     * @param string    $identifier             the identifier of the wallet
1409
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1410
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1411
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1412
     * @return mixed
1413
     */
1414
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1415
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1416
            'checksum' => $checksumAddress,
1417
            'signature' => $signature
1418
        ], RestClient::AUTH_HTTP_SIG, 360);
1419
        return self::jsonDecode($response->body(), true);
1420
    }
1421
1422
    /**
1423
     * create new backup key;
1424
     *  1) a BIP39 mnemonic
1425
     *  2) a seed from that mnemonic with a blank password
1426
     *  3) a private key from that seed
1427
     *
1428
     * @return array [mnemonic, seed, key]
1429
     */
1430
    protected function newV1BackupSeed() {
1431
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1432
1433
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1434
    }
1435
1436
    /**
1437
     * create new primary key;
1438
     *  1) a BIP39 mnemonic
1439
     *  2) a seed from that mnemonic with the password
1440
     *  3) a private key from that seed
1441
     *
1442
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1443
     * @return array [mnemonic, seed, key]
1444
     * @TODO: require a strong password?
1445
     */
1446
    protected function newV1PrimarySeed($passphrase) {
1447
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1448
1449
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1450
    }
1451
1452
    /**
1453
     * create a new key;
1454
     *  1) a BIP39 mnemonic
1455
     *  2) a seed from that mnemonic with the password
1456
     *  3) a private key from that seed
1457
     *
1458
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1459
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1460
     * @return array
1461
     */
1462
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1463
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1464
        do {
1465
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1466
1467
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1468
1469
            $key = null;
1470
            try {
1471
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1472
            } catch (\Exception $e) {
1473
                // try again
1474
            }
1475
        } while (!$key);
1476
1477
        return [$mnemonic, $seed, $key];
1478
    }
1479
1480
    /**
1481
     * generate a new mnemonic from some random entropy (512 bit)
1482
     *
1483
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1484
     * @return string
1485
     * @throws \Exception
1486
     */
1487
    public function generateNewMnemonic($forceEntropy = null) {
1488
        if ($forceEntropy === null) {
1489
            $random = new Random();
1490
            $entropy = $random->bytes(512 / 8);
1491
        } else {
1492
            $entropy = $forceEntropy;
1493
        }
1494
1495
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1492 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...
1496
    }
1497
1498
    /**
1499
     * get the balance for the wallet
1500
     *
1501
     * @param string    $identifier             the identifier of the wallet
1502
     * @return array
1503
     */
1504
    public function getWalletBalance($identifier) {
1505
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1506
        return self::jsonDecode($response->body(), true);
1507
    }
1508
1509
    /**
1510
     * get a new derivation number for specified parent path
1511
     *  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
1512
     *
1513
     * returns the path
1514
     *
1515
     * @param string    $identifier             the identifier of the wallet
1516
     * @param string    $path                   the parent path for which to get a new derivation
1517
     * @return string
1518
     */
1519
    public function getNewDerivation($identifier, $path) {
1520
        $result = $this->_getNewDerivation($identifier, $path);
1521
        return $result['path'];
1522
    }
1523
1524
    /**
1525
     * get a new derivation number for specified parent path
1526
     *  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
1527
     *
1528
     * @param string    $identifier             the identifier of the wallet
1529
     * @param string    $path                   the parent path for which to get a new derivation
1530
     * @return mixed
1531
     */
1532
    public function _getNewDerivation($identifier, $path) {
1533
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1534
        return self::jsonDecode($response->body(), true);
1535
    }
1536
1537
    /**
1538
     * get the path (and redeemScript) to specified address
1539
     *
1540
     * @param string $identifier
1541
     * @param string $address
1542
     * @return array
1543
     * @throws \Exception
1544
     */
1545
    public function getPathForAddress($identifier, $address) {
1546
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1547
        return self::jsonDecode($response->body(), true)['path'];
1548
    }
1549
1550
    /**
1551
     * send the transaction using the API
1552
     *
1553
     * @param string       $identifier     the identifier of the wallet
1554
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1555
     * @param array        $paths          list of the paths that were used for the UTXO
1556
     * @param bool         $checkFee       let the server verify the fee after signing
1557
     * @param null         $twoFactorToken
1558
     * @return string                                the complete raw transaction
1559
     * @throws \Exception
1560
     */
1561
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1562
        $data = [
1563
            'paths' => $paths,
1564
            'two_factor_token' => $twoFactorToken,
1565
        ];
1566
1567
        if (is_array($rawTransaction)) {
1568
            if (array_key_exists('base_transaction', $rawTransaction)
1569
            && array_key_exists('signed_transaction', $rawTransaction)) {
1570
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1571
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1572
            } else {
1573
                throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1574
            }
1575
        } else {
1576
            $data['raw_transaction'] = $rawTransaction;
1577
        }
1578
1579
        // dynamic TTL for when we're signing really big transactions
1580
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1581
1582
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1583
        $signed = self::jsonDecode($response->body(), true);
1584
1585
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1586
            throw new \Exception("Failed to completely sign transaction");
1587
        }
1588
1589
        // create TX hash from the raw signed hex
1590
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1591
    }
1592
1593
    /**
1594
     * use the API to get the best inputs to use based on the outputs
1595
     *
1596
     * the return array has the following format:
1597
     * [
1598
     *  "utxos" => [
1599
     *      [
1600
     *          "hash" => "<txHash>",
1601
     *          "idx" => "<index of the output of that <txHash>",
1602
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1603
     *          "value" => 32746327,
1604
     *          "address" => "1address",
1605
     *          "path" => "m/44'/1'/0'/0/13",
1606
     *          "redeem_script" => "<redeemScript-hex>",
1607
     *      ],
1608
     *  ],
1609
     *  "fee"   => 10000,
1610
     *  "change"=> 1010109201,
1611
     * ]
1612
     *
1613
     * @param string   $identifier              the identifier of the wallet
1614
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1615
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1616
     *                                          so you have some time to spend them without race-conditions
1617
     * @param bool     $allowZeroConf
1618
     * @param string   $feeStrategy
1619
     * @param null|int $forceFee
1620
     * @return array
1621
     * @throws \Exception
1622
     */
1623
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1624
        $args = [
1625
            'lock' => (int)!!$lockUTXO,
1626
            'zeroconf' => (int)!!$allowZeroConf,
1627
            'fee_strategy' => $feeStrategy,
1628
        ];
1629
1630
        if ($forceFee !== null) {
1631
            $args['forcefee'] = (int)$forceFee;
1632
        }
1633
1634
        $response = $this->blocktrailClient->post(
1635
            "wallet/{$identifier}/coin-selection",
1636
            $args,
1637
            $outputs,
1638
            RestClient::AUTH_HTTP_SIG
1639
        );
1640
1641
        \var_export(self::jsonDecode($response->body(), true));
1642
1643
        return self::jsonDecode($response->body(), true);
1644
    }
1645
1646
    /**
1647
     *
1648
     * @param string   $identifier the identifier of the wallet
1649
     * @param bool     $allowZeroConf
1650
     * @param string   $feeStrategy
1651
     * @param null|int $forceFee
1652
     * @param int      $outputCnt
1653
     * @return array
1654
     * @throws \Exception
1655
     */
1656
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1657
        $args = [
1658
            'zeroconf' => (int)!!$allowZeroConf,
1659
            'fee_strategy' => $feeStrategy,
1660
            'outputs' => $outputCnt,
1661
        ];
1662
1663
        if ($forceFee !== null) {
1664
            $args['forcefee'] = (int)$forceFee;
1665
        }
1666
1667
        $response = $this->blocktrailClient->get(
1668
            "wallet/{$identifier}/max-spendable",
1669
            $args,
1670
            RestClient::AUTH_HTTP_SIG
1671
        );
1672
1673
        return self::jsonDecode($response->body(), true);
1674
    }
1675
1676
    /**
1677
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1678
     */
1679
    public function feePerKB() {
1680
        $response = $this->blocktrailClient->get("fee-per-kb");
1681
        return self::jsonDecode($response->body(), true);
1682
    }
1683
1684
    /**
1685
     * get the current price index
1686
     *
1687
     * @return array        eg; ['USD' => 287.30]
1688
     */
1689 2
    public function price() {
1690 2
        $response = $this->blocktrailClient->get("price");
1691 1
        return self::jsonDecode($response->body(), true);
1692
    }
1693
1694
    /**
1695
     * setup webhook for wallet
1696
     *
1697
     * @param string    $identifier         the wallet identifier for which to create the webhook
1698
     * @param string    $webhookIdentifier  the webhook identifier to use
1699
     * @param string    $url                the url to receive the webhook events
1700
     * @return array
1701
     */
1702
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1703
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1704
        return self::jsonDecode($response->body(), true);
1705
    }
1706
1707
    /**
1708
     * delete webhook for wallet
1709
     *
1710
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1711
     * @param string    $webhookIdentifier  the webhook identifier to delete
1712
     * @return array
1713
     */
1714
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1715
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1716
        return self::jsonDecode($response->body(), true);
1717
    }
1718
1719
    /**
1720
     * lock a specific unspent output
1721
     *
1722
     * @param     $identifier
1723
     * @param     $txHash
1724
     * @param     $txIdx
1725
     * @param int $ttl
1726
     * @return bool
1727
     */
1728
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1729
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1730
        return self::jsonDecode($response->body(), true)['locked'];
1731
    }
1732
1733
    /**
1734
     * unlock a specific unspent output
1735
     *
1736
     * @param     $identifier
1737
     * @param     $txHash
1738
     * @param     $txIdx
1739
     * @return bool
1740
     */
1741
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1742
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1743
        return self::jsonDecode($response->body(), true)['unlocked'];
1744
    }
1745
1746
    /**
1747
     * get all transactions for wallet (paginated)
1748
     *
1749
     * @param  string  $identifier  the wallet identifier for which to get transactions
1750
     * @param  integer $page        pagination: page number
1751
     * @param  integer $limit       pagination: records per page (max 500)
1752
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1753
     * @return array                associative array containing the response
1754
     */
1755
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1756
        $queryString = [
1757
            'page' => $page,
1758
            'limit' => $limit,
1759
            'sort_dir' => $sortDir
1760
        ];
1761
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1762
        return self::jsonDecode($response->body(), true);
1763
    }
1764
1765
    /**
1766
     * get all addresses for wallet (paginated)
1767
     *
1768
     * @param  string  $identifier  the wallet identifier for which to get addresses
1769
     * @param  integer $page        pagination: page number
1770
     * @param  integer $limit       pagination: records per page (max 500)
1771
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1772
     * @return array                associative array containing the response
1773
     */
1774
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1775
        $queryString = [
1776
            'page' => $page,
1777
            'limit' => $limit,
1778
            'sort_dir' => $sortDir
1779
        ];
1780
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1781
        return self::jsonDecode($response->body(), true);
1782
    }
1783
1784
    /**
1785
     * get all UTXOs for wallet (paginated)
1786
     *
1787
     * @param  string  $identifier  the wallet identifier for which to get addresses
1788
     * @param  integer $page        pagination: page number
1789
     * @param  integer $limit       pagination: records per page (max 500)
1790
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1791
     * @param  boolean $zeroconf    include zero confirmation transactions
1792
     * @return array                associative array containing the response
1793
     */
1794
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1795
        $queryString = [
1796
            'page' => $page,
1797
            'limit' => $limit,
1798
            'sort_dir' => $sortDir,
1799
            'zeroconf' => (int)!!$zeroconf,
1800
        ];
1801
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1802
        return self::jsonDecode($response->body(), true);
1803
    }
1804
1805
    /**
1806
     * get a paginated list of all wallets associated with the api user
1807
     *
1808
     * @param  integer          $page    pagination: page number
1809
     * @param  integer          $limit   pagination: records per page
1810
     * @return array                     associative array containing the response
1811
     */
1812
    public function allWallets($page = 1, $limit = 20) {
1813
        $queryString = [
1814
            'page' => $page,
1815
            'limit' => $limit
1816
        ];
1817
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1818
        return self::jsonDecode($response->body(), true);
1819
    }
1820
1821
    /**
1822
     * send raw transaction
1823
     *
1824
     * @param     $txHex
1825
     * @return bool
1826
     */
1827
    public function sendRawTransaction($txHex) {
1828
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1829
        return self::jsonDecode($response->body(), true);
1830
    }
1831
1832
    /**
1833
     * testnet only ;-)
1834
     *
1835
     * @param     $address
1836
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1837
     * @return mixed
1838
     * @throws \Exception
1839
     */
1840
    public function faucetWithdrawal($address, $amount = 10000) {
1841
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1842
            'address' => $address,
1843
            'amount' => $amount,
1844
        ], RestClient::AUTH_HTTP_SIG);
1845
        return self::jsonDecode($response->body(), true);
1846
    }
1847
1848
    /**
1849
     * Exists for BC. Remove at major bump.
1850
     *
1851
     * @see faucetWithdrawal
1852
     * @deprecated
1853
     * @param     $address
1854
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1855
     * @return mixed
1856
     * @throws \Exception
1857
     */
1858
    public function faucetWithdrawl($address, $amount = 10000) {
1859
        return $this->faucetWithdrawal($address, $amount);
1860
    }
1861
1862
    /**
1863
     * verify a message signed bitcoin-core style
1864
     *
1865
     * @param  string           $message
1866
     * @param  string           $address
1867
     * @param  string           $signature
1868
     * @return boolean
1869
     */
1870 2
    public function verifyMessage($message, $address, $signature) {
1871 2
        $adapter = Bitcoin::getEcAdapter();
1872 2
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1873 2
        if (!$addr instanceof PayToPubKeyHashAddress) {
1874
            throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
1875
        }
1876
1877
        /** @var CompactSignatureSerializerInterface $csSerializer */
1878 2
        $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...
1879 2
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1880
1881 2
        $signer = new MessageSigner($adapter);
1882 2
        return $signer->verify($signedMessage, $addr);
1883
    }
1884
1885
    /**
1886
     * Take a base58 or cashaddress, and return only
1887
     * the cash address.
1888
     * This function only works on bitcoin cash.
1889
     * @param string $input
1890
     * @return string
1891
     * @throws BlocktrailSDKException
1892
     */
1893
    public function getLegacyBitcoinCashAddress($input) {
1894
        if ($this->network === "bitcoincash") {
1895
            $address = $this
1896
                ->makeAddressReader([
1897
                    "use_cashaddress" => true
1898
                ])
1899
                ->fromString($input);
1900
1901
            if ($address instanceof CashAddress) {
1902
                $address = $address->getLegacyAddress();
1903
            }
1904
1905
            return $address->getAddress();
1906
        }
1907
1908
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1909
    }
1910
1911
    /**
1912
     * convert a Satoshi value to a BTC value
1913
     *
1914
     * @param int       $satoshi
1915
     * @return float
1916
     */
1917 1
    public static function toBTC($satoshi) {
1918 1
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1919
    }
1920
1921
    /**
1922
     * convert a Satoshi value to a BTC value and return it as a string
1923
1924
     * @param int       $satoshi
1925
     * @return string
1926
     */
1927
    public static function toBTCString($satoshi) {
1928
        return sprintf("%.8f", self::toBTC($satoshi));
1929
    }
1930
1931
    /**
1932
     * convert a BTC value to a Satoshi value
1933
     *
1934
     * @param float     $btc
1935
     * @return string
1936
     */
1937 1
    public static function toSatoshiString($btc) {
1938 1
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1939
    }
1940
1941
    /**
1942
     * convert a BTC value to a Satoshi value
1943
     *
1944
     * @param float     $btc
1945
     * @return string
1946
     */
1947 1
    public static function toSatoshi($btc) {
1948 1
        return (int)self::toSatoshiString($btc);
1949
    }
1950
1951
    /**
1952
     * json_decode helper that throws exceptions when it fails to decode
1953
     *
1954
     * @param      $json
1955
     * @param bool $assoc
1956
     * @return mixed
1957
     * @throws \Exception
1958
     */
1959 4
    public static function jsonDecode($json, $assoc = false) {
1960 4
        if (!$json) {
1961
            throw new \Exception("Can't json_decode empty string [{$json}]");
1962
        }
1963
1964 4
        $data = json_decode($json, $assoc);
1965
1966 4
        if ($data === null) {
1967
            throw new \Exception("Failed to json_decode [{$json}]");
1968
        }
1969
1970 4
        return $data;
1971
    }
1972
1973
    /**
1974
     * sort public keys for multisig script
1975
     *
1976
     * @param PublicKeyInterface[] $pubKeys
1977
     * @return PublicKeyInterface[]
1978
     */
1979
    public static function sortMultisigKeys(array $pubKeys) {
1980
        $result = array_values($pubKeys);
1981
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1982
            $av = $a->getHex();
1983
            $bv = $b->getHex();
1984
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1985
        });
1986
1987
        return $result;
1988
    }
1989
1990
    /**
1991
     * read and decode the json payload from a webhook's POST request.
1992
     *
1993
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1994
     * @return mixed|null
1995
     * @throws \Exception
1996
     */
1997
    public static function getWebhookPayload($returnObject = false) {
1998
        $data = file_get_contents("php://input");
1999
        if ($data) {
2000
            return self::jsonDecode($data, !$returnObject);
2001
        } else {
2002
            return null;
2003
        }
2004
    }
2005
2006
    public static function normalizeBIP32KeyArray($keys) {
2007
        return Util::arrayMapWithIndex(function ($idx, $key) {
2008
            return [$idx, self::normalizeBIP32Key($key)];
2009
        }, $keys);
2010
    }
2011
2012
    /**
2013
     * @param array|BIP32Key $key
2014
     * @return BIP32Key
2015
     * @throws \Exception
2016
     */
2017
    public static function normalizeBIP32Key($key) {
2018
        if ($key instanceof BIP32Key) {
2019
            return $key;
2020
        }
2021
2022
        if (is_array($key) && count($key) === 2) {
2023
            $path = $key[1];
2024
            $hk = $key[0];
2025
2026
            if (!($hk instanceof HierarchicalKey)) {
2027
                $hk = HierarchicalKeyFactory::fromExtended($hk);
2028
            }
2029
2030
            return BIP32Key::create($hk, $path);
2031
        } else {
2032
            throw new \Exception("Bad Input");
2033
        }
2034
    }
2035
2036
    public function shuffle($arr) {
2037
        \shuffle($arr);
2038
    }
2039
}
2040