Completed
Pull Request — master (#118)
by Ruben de
05:52
created

BlocktrailSDK::faucetWithdrawl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
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 19
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
79
80 19
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
81
82 19
        if (is_null($apiEndpoint)) {
83 19
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
84 19
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
85
        }
86
87
        // normalize network and set bitcoinlib to the right magic-bytes
88 19
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
89 19
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
90
91 19
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT');
92 19
        if (!$btccomEndpoint) {
93
            $btccomEndpoint = "https://" . ($this->network === "BCC" ? "bch-chain" : "chain") . ".api.btc.com";
94
        }
95 19
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
96
97 19
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
98 19
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
99
        }
100
101 19
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
102 19
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
103
104 19
        $this->converter = new BtccomConverter();
105 19
    }
106
107
    /**
108
     * normalize network string
109
     *
110
     * @param $network
111
     * @param $testnet
112
     * @return array
113
     * @throws \Exception
114
     */
115 19
    protected function normalizeNetwork($network, $testnet) {
116
        // [name, testnet, network]
117 19
        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 19
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
128
129 19
        if ($network === "bitcoin") {
130 16
            if ($regtest) {
131 16
                $useNetwork = NetworkFactory::bitcoinRegtest();
132
            } else if ($testnet) {
133
                $useNetwork = NetworkFactory::bitcoinTestnet();
134
            } else {
135 16
                $useNetwork = NetworkFactory::bitcoin();
136
            }
137 3
        } else if ($network === "bitcoincash") {
138 3
            if ($regtest) {
139 3
                $useNetwork = new BitcoinCashRegtest();
140
            } else if ($testnet) {
141
                $useNetwork = new BitcoinCashTestnet();
142
            } else {
143
                $useNetwork = new BitcoinCash();
144
            }
145
        }
146
147 19
        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 19
    }
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
    public function getRestClient() {
190
        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->client = $restClient;
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...
205
    }
206
207
    /**
208
     * get a single address
209
     * @param  string $address address hash
210
     * @return array           associative array containing the response
211
     */
212
    public function address($address) {
213
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
214
        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
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
226
        $queryString = [
227
            'page' => $page,
228
            'limit' => $limit,
229
            'sort_dir' => $sortDir
230
        ];
231
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
232
        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
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
262
        $queryString = [
263
            'page' => $page,
264
            'limit' => $limit,
265
            'sort_dir' => $sortDir
266
        ];
267
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
268
        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
    public function verifyAddress($address, $signature) {
313
        if ($this->verifyMessage($address, $address, $signature)) {
314
            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
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
328
        $queryString = [
329
            'page' => $page,
330
            'limit' => $limit,
331
            'sort_dir' => $sortDir
332
        ];
333
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
334
        return $this->converter->convertBlocks($response->body());
335
    }
336
337
    /**
338
     * get the latest block
339
     * @return array            associative array containing the response
340
     */
341
    public function blockLatest() {
342
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
343
        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
    public function block($block) {
352
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
353
        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
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
365
        $queryString = [
366
            'page' => $page,
367
            'limit' => $limit,
368
            'sort_dir' => $sortDir
369
        ];
370
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
371
        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
    public function transaction($txhash) {
380
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
381
        $res = $this->converter->convertTx($response->body(), null);
382
383
        if ($this->converter instanceof BtccomConverter) {
384
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
385
        }
386
387
        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
    public function transactions($txhashes) {
396
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
397
        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 2
    public function createNewWallet($options) {
602 2
        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 2
        if (isset($options['password'])) {
612 2
            if (isset($options['passphrase'])) {
613
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
614
            } else {
615 2
                $options['passphrase'] = $options['password'];
616
            }
617
        }
618
619 2
        if (!isset($options['passphrase'])) {
620
            $options['passphrase'] = null;
621
        }
622
623 2
        if (!isset($options['key_index'])) {
624
            $options['key_index'] = 0;
625
        }
626
627 2
        if (!isset($options['wallet_version'])) {
628 1
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
629
        }
630
631 2
        switch ($options['wallet_version']) {
632 2
            case Wallet::WALLET_VERSION_V1:
633 1
                return $this->createNewWalletV1($options);
634
635 1
            case Wallet::WALLET_VERSION_V2:
636
                return $this->createNewWalletV2($options);
637
638 1
            case Wallet::WALLET_VERSION_V3:
639 1
                return $this->createNewWalletV3($options);
640
641
            default:
642
                throw new \InvalidArgumentException("Invalid wallet version");
643
        }
644
    }
645
646 1
    protected function createNewWalletV1($options) {
647 1
        $walletPath = WalletPath::create($options['key_index']);
648
649 1
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
650
651 1
        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 1
        $primaryMnemonic = null;
656 1
        $primaryPrivateKey = null;
657 1
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
658 1
            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 1
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
664 1
                if ($storePrimaryMnemonic !== false) {
665 1
                    $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 1
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
675
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
676
        }
677
678 1
        if ($primaryPrivateKey) {
679 1
            if (is_string($primaryPrivateKey)) {
680 1
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
681
            }
682
        } else {
683
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
684
        }
685
686 1
        if (!$storePrimaryMnemonic) {
687
            $primaryMnemonic = false;
688
        }
689
690
        // create primary public key from the created private key
691 1
        $path = $walletPath->keyIndexPath()->publicPath();
692 1
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
693
694 1
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
695
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
696
        }
697
698 1
        $backupMnemonic = null;
699 1
        $backupPublicKey = null;
700 1
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
701
            /** @var HierarchicalKey $backupPrivateKey */
702 1
            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 1
        if ($backupPublicKey) {
710
            if (is_string($backupPublicKey)) {
711
                $backupPublicKey = [$backupPublicKey, "m"];
712
            }
713
        } else {
714 1
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
715 1
            $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 1
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
720 1
        $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 1
        $data = $this->storeNewWalletV1(
725 1
            $options['identifier'],
726 1
            $primaryPublicKey->tuple(),
727 1
            $backupPublicKey->tuple(),
728 1
            $primaryMnemonic,
729 1
            $checksum,
730 1
            $options['key_index'],
731 1
            array_key_exists('segwit', $options) ? $options['segwit'] : false
732
        );
733
734
        // received the blocktrail public keys
735 1
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
736 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
737 1
        }, $data['blocktrail_public_keys']);
738
739 1
        $wallet = new WalletV1(
740 1
            $this,
741 1
            $options['identifier'],
742 1
            $primaryMnemonic,
743 1
            [$options['key_index'] => $primaryPublicKey],
744 1
            $backupPublicKey,
745 1
            $blocktrailPublicKeys,
746 1
            $options['key_index'],
747 1
            $this->network,
748 1
            $this->testnet,
749 1
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
750 1
            $addressReader,
751 1
            $checksum
752
        );
753
754 1
        $wallet->unlock($options);
755
756
        // return wallet and backup mnemonic
757
        return [
758 1
            $wallet,
759
            [
760 1
                'primary_mnemonic' => $primaryMnemonic,
761 1
                'backup_mnemonic' => $backupMnemonic,
762 1
                '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->randomBits(256);
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
                $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
811
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
812
            } else {
813
                $secret = $options['secret'];
814
            }
815
816
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
817
            $recoverySecret = bin2hex($this->randomBits(256));
818
819
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
820
        }
821
822
        if (!isset($options['backup_public_key'])) {
823
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->randomBits(256);
824
        }
825
826
        if (isset($options['primary_private_key'])) {
827
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
828
        } else {
829
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
830
        }
831
832
        // create primary public key from the created private key
833
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
834
835
        if (!isset($options['backup_public_key'])) {
836
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
837
        }
838
839
        // create a checksum of our private key which we'll later use to verify we used the right password
840
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
841
        $addressReader = $this->makeAddressReader($options);
842
843
        // send the public keys and encrypted data to server
844
        $data = $this->storeNewWalletV2(
845
            $options['identifier'],
846
            $options['primary_public_key']->tuple(),
847
            $options['backup_public_key']->tuple(),
848
            $storeDataOnServer ? $encryptedPrimarySeed : false,
849
            $storeDataOnServer ? $encryptedSecret : false,
850
            $storeDataOnServer ? $recoverySecret : false,
851
            $checksum,
852
            $options['key_index'],
853
            array_key_exists('segwit', $options) ? $options['segwit'] : false
854
        );
855
856
        // received the blocktrail public keys
857
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
858
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
859
        }, $data['blocktrail_public_keys']);
860
861
        $wallet = new WalletV2(
862
            $this,
863
            $options['identifier'],
864
            $encryptedPrimarySeed,
865
            $encryptedSecret,
866
            [$options['key_index'] => $options['primary_public_key']],
867
            $options['backup_public_key'],
868
            $blocktrailPublicKeys,
869
            $options['key_index'],
870
            $this->network,
871
            $this->testnet,
872
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
873
            $addressReader,
874
            $checksum
875
        );
876
877
        $wallet->unlock([
878
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
879
            'primary_private_key' => $options['primary_private_key'],
880
            'primary_seed' => $primarySeed,
881
            'secret' => $secret,
882
        ]);
883
884
        // return wallet and mnemonics for backup sheet
885
        return [
886
            $wallet,
887
            [
888
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
889
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
890
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
891
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
892
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
893
                    return [$keyIndex, $pubKey->tuple()];
894
                }, $blocktrailPublicKeys),
895
            ],
896
        ];
897
    }
898
899 1
    protected function createNewWalletV3($options) {
900 1
        $walletPath = WalletPath::create($options['key_index']);
901
902 1
        if (isset($options['store_primary_mnemonic'])) {
903
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
904
        }
905
906 1
        if (!isset($options['store_data_on_server'])) {
907 1
            if (isset($options['primary_private_key'])) {
908
                $options['store_data_on_server'] = false;
909
            } else {
910 1
                $options['store_data_on_server'] = true;
911
            }
912
        }
913
914 1
        $storeDataOnServer = $options['store_data_on_server'];
915
916 1
        $secret = null;
917 1
        $encryptedSecret = null;
918 1
        $primarySeed = null;
919 1
        $encryptedPrimarySeed = null;
920 1
        $recoverySecret = null;
921 1
        $recoveryEncryptedSecret = null;
922 1
        $backupSeed = null;
923
924 1
        if (!isset($options['primary_private_key'])) {
925 1
            if (isset($options['primary_seed'])) {
926
                if (!$options['primary_seed'] instanceof BufferInterface) {
927
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
928
                }
929
                $primarySeed = $options['primary_seed'];
930
            } else {
931 1
                $primarySeed = $this->newPrimarySeed();
932
            }
933
        }
934
935 1
        if ($storeDataOnServer) {
936 1
            if (!isset($options['secret'])) {
937 1
                if (!$options['passphrase']) {
938
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
939
                }
940
941 1
                list($secret, $encryptedSecret) = $this->newSecret($options['passphrase']);
942
            } else {
943
                if (!$options['secret'] instanceof Buffer) {
944
                    throw new \InvalidArgumentException('Secret must be provided as a Buffer');
945
                }
946
947
                $secret = $options['secret'];
948
            }
949
950 1
            $encryptedPrimarySeed = $this->newEncryptedPrimarySeed($primarySeed, $secret);
951 1
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newRecoverySecret($secret);
952
        }
953
954 1
        if (!isset($options['backup_public_key'])) {
955 1
            if (isset($options['backup_seed'])) {
956
                if (!$options['backup_seed'] instanceof Buffer) {
957
                    throw new \InvalidArgumentException('Backup seed must be an instance of Buffer');
958
                }
959
                $backupSeed = $options['backup_seed'];
960
            } else {
961 1
                $backupSeed = $this->newBackupSeed();
962
            }
963
        }
964
965 1
        if (isset($options['primary_private_key'])) {
966
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
967
        } else {
968 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
969
        }
970
971
        // create primary public key from the created private key
972 1
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
973
974 1
        if (!isset($options['backup_public_key'])) {
975 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
976
        }
977
978
        // create a checksum of our private key which we'll later use to verify we used the right password
979 1
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
980 1
        $addressReader = $this->makeAddressReader($options);
981
982
        // send the public keys and encrypted data to server
983 1
        $data = $this->storeNewWalletV3(
984 1
            $options['identifier'],
985 1
            $options['primary_public_key']->tuple(),
986 1
            $options['backup_public_key']->tuple(),
987 1
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
988 1
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
989 1
            $storeDataOnServer ? $recoverySecret->getHex() : false,
990 1
            $checksum,
991 1
            $options['key_index'],
992 1
            array_key_exists('segwit', $options) ? $options['segwit'] : false
993
        );
994
995
        // received the blocktrail public keys
996 1
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
997 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
998 1
        }, $data['blocktrail_public_keys']);
999
1000 1
        $wallet = new WalletV3(
1001 1
            $this,
1002 1
            $options['identifier'],
1003 1
            $encryptedPrimarySeed,
1004 1
            $encryptedSecret,
1005 1
            [$options['key_index'] => $options['primary_public_key']],
1006 1
            $options['backup_public_key'],
1007 1
            $blocktrailPublicKeys,
1008 1
            $options['key_index'],
1009 1
            $this->network,
1010 1
            $this->testnet,
1011 1
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1012 1
            $addressReader,
1013 1
            $checksum
1014
        );
1015
1016 1
        $wallet->unlock([
1017 1
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1018 1
            'primary_private_key' => $options['primary_private_key'],
1019 1
            'primary_seed' => $primarySeed,
1020 1
            'secret' => $secret,
1021
        ]);
1022
1023
        // return wallet and mnemonics for backup sheet
1024
        return [
1025 1
            $wallet,
1026
            [
1027 1
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1028 1
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1029 1
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1030 1
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1031 1
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1032 1
                    return [$keyIndex, $pubKey->tuple()];
1033 1
                }, $blocktrailPublicKeys),
1034
            ]
1035
        ];
1036
    }
1037
1038
    public function newPrimarySeed() {
1039
        return new Buffer($this->randomBits(256));
1040
    }
1041
1042
    public function newBackupSeed() {
1043
        return new Buffer($this->randomBits(256));
1044
    }
1045
1046
    public function newSecret($passphrase) {
1047
        $secret = new Buffer($this->randomBits(256));
1048
        $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
1049
            ->getBuffer();
1050
1051
        return [$secret, $encryptedSecret];
1052
    }
1053
1054
    public function newEncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
1055
        return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
1056
            ->getBuffer();
1057
    }
1058
1059
    public function newRecoverySecret(Buffer $secret) {
1060
        $recoverySecret = new Buffer($this->randomBits(256));
1061
        $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
1062
            ->getBuffer();
1063
1064
        return [$recoverySecret, $recoveryEncryptedSecret];
1065
    }
1066
1067
    /**
1068
     * @param array $bip32Key
1069
     * @throws BlocktrailSDKException
1070
     */
1071
    private function verifyPublicBIP32Key(array $bip32Key) {
1072
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1073
        if ($hk->isPrivate()) {
1074
            throw new BlocktrailSDKException('Private key was included in request, abort');
1075
        }
1076
1077
        if (substr($bip32Key[1], 0, 1) === "m") {
1078
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1079
        }
1080
    }
1081
1082
    /**
1083
     * @param array $walletData
1084
     * @throws BlocktrailSDKException
1085
     */
1086
    private function verifyPublicOnly(array $walletData) {
1087
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1088
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1089
    }
1090
1091
    /**
1092
     * create wallet using the API
1093
     *
1094
     * @param string    $identifier             the wallet identifier to create
1095
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1096
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1097
     * @param string    $primaryMnemonic        mnemonic to store
1098
     * @param string    $checksum               checksum to store
1099
     * @param int       $keyIndex               account that we expect to use
1100
     * @param bool      $segwit                 opt in to segwit
1101
     * @return mixed
1102
     */
1103
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1104
        $data = [
1105
            'identifier' => $identifier,
1106
            'primary_public_key' => $primaryPublicKey,
1107
            'backup_public_key' => $backupPublicKey,
1108
            'primary_mnemonic' => $primaryMnemonic,
1109
            'checksum' => $checksum,
1110
            'key_index' => $keyIndex,
1111
            'segwit' => $segwit,
1112
        ];
1113
        $this->verifyPublicOnly($data);
1114
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1115
        return self::jsonDecode($response->body(), true);
1116
    }
1117
1118
    /**
1119
     * create wallet using the API
1120
     *
1121
     * @param string $identifier       the wallet identifier to create
1122
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1123
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1124
     * @param        $encryptedPrimarySeed
1125
     * @param        $encryptedSecret
1126
     * @param        $recoverySecret
1127
     * @param string $checksum         checksum to store
1128
     * @param int    $keyIndex         account that we expect to use
1129
     * @param bool   $segwit           opt in to segwit
1130
     * @return mixed
1131
     * @throws \Exception
1132
     */
1133
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1134
        $data = [
1135
            'identifier' => $identifier,
1136
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1137
            'primary_public_key' => $primaryPublicKey,
1138
            'backup_public_key' => $backupPublicKey,
1139
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1140
            'encrypted_secret' => $encryptedSecret,
1141
            'recovery_secret' => $recoverySecret,
1142
            'checksum' => $checksum,
1143
            'key_index' => $keyIndex,
1144
            'segwit' => $segwit,
1145
        ];
1146
        $this->verifyPublicOnly($data);
1147
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1148
        return self::jsonDecode($response->body(), true);
1149
    }
1150
1151
    /**
1152
     * create wallet using the API
1153
     *
1154
     * @param string $identifier       the wallet identifier to create
1155
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1156
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1157
     * @param        $encryptedPrimarySeed
1158
     * @param        $encryptedSecret
1159
     * @param        $recoverySecret
1160
     * @param string $checksum         checksum to store
1161
     * @param int    $keyIndex         account that we expect to use
1162
     * @param bool   $segwit           opt in to segwit
1163
     * @return mixed
1164
     * @throws \Exception
1165
     */
1166
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1167
1168
        $data = [
1169
            'identifier' => $identifier,
1170
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1171
            'primary_public_key' => $primaryPublicKey,
1172
            'backup_public_key' => $backupPublicKey,
1173
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1174
            'encrypted_secret' => $encryptedSecret,
1175
            'recovery_secret' => $recoverySecret,
1176
            'checksum' => $checksum,
1177
            'key_index' => $keyIndex,
1178
            'segwit' => $segwit,
1179
        ];
1180
1181
        $this->verifyPublicOnly($data);
1182
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1183
        return self::jsonDecode($response->body(), true);
1184
    }
1185
1186
    /**
1187
     * upgrade wallet to use a new account number
1188
     *  the account number specifies which blocktrail cosigning key is used
1189
     *
1190
     * @param string    $identifier             the wallet identifier to be upgraded
1191
     * @param int       $keyIndex               the new account to use
1192
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1193
     * @return mixed
1194
     */
1195
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1196
        $data = [
1197
            'key_index' => $keyIndex,
1198
            'primary_public_key' => $primaryPublicKey
1199
        ];
1200
1201
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1202
        return self::jsonDecode($response->body(), true);
1203
    }
1204
1205
    /**
1206
     * @param array $options
1207
     * @return AddressReaderBase
1208
     */
1209 17
    private function makeAddressReader(array $options) {
1210 17
        if ($this->network == "bitcoincash") {
1211 3
            $useCashAddress = false;
1212 3
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1213 1
                $useCashAddress = true;
1214
            }
1215 3
            return new BitcoinCashAddressReader($useCashAddress);
1216
        } else {
1217 14
            return new BitcoinAddressReader();
1218
        }
1219
    }
1220
1221
    /**
1222
     * initialize a previously created wallet
1223
     *
1224
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1225
     *
1226
     * Some of the options:
1227
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1228
     *    so the wallet is loaded in read-only mode (no private key)
1229
     *  - "check_backup_key" can be set to your own backup key:
1230
     *    Format: ["M', "xpub..."]
1231
     *    Setting this will allow the SDK to check the server hasn't
1232
     *    a different key (one it happens to control)
1233
1234
     * Either takes one argument:
1235
     * @param array $options
1236
     *
1237
     * Or takes two arguments (old, deprecated syntax):
1238
     * (@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...
1239
     * (@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...
1240
     *
1241
     * @return WalletInterface
1242
     * @throws \Exception
1243
     */
1244 17
    public function initWallet($options) {
1245 17
        if (!is_array($options)) {
1246
            $args = func_get_args();
1247
            $options = [
1248
                "identifier" => $args[0],
1249
                "password" => $args[1],
1250
            ];
1251
        }
1252
1253 17
        $identifier = $options['identifier'];
1254 17
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1255 17
                    (isset($options['readOnly']) ? $options['readOnly'] :
1256 17
                        (isset($options['read-only']) ? $options['read-only'] :
1257 17
                            false));
1258
1259
        // get the wallet data from the server
1260 17
        $data = $this->getWallet($identifier);
1261 17
        if (!$data) {
1262
            throw new \Exception("Failed to get wallet");
1263
        }
1264
1265 17
        if (array_key_exists('check_backup_key', $options)) {
1266 3
            if (!is_string($options['check_backup_key'])) {
1267 1
                throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
1268
            }
1269 2
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1270 1
                throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
1271
            }
1272
        }
1273
1274 15
        $addressReader = $this->makeAddressReader($options);
1275
1276 15
        switch ($data['wallet_version']) {
1277 15
            case Wallet::WALLET_VERSION_V1:
1278
                $wallet = new WalletV1(
1279
                    $this,
1280
                    $identifier,
1281
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1282
                    $data['primary_public_keys'],
1283
                    $data['backup_public_key'],
1284
                    $data['blocktrail_public_keys'],
1285
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1286
                    $this->network,
1287
                    $this->testnet,
1288
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1289
                    $addressReader,
1290
                    $data['checksum']
1291
                );
1292
                break;
1293 15
            case Wallet::WALLET_VERSION_V2:
1294
                $wallet = new WalletV2(
1295
                    $this,
1296
                    $identifier,
1297
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1298
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1299
                    $data['primary_public_keys'],
1300
                    $data['backup_public_key'],
1301
                    $data['blocktrail_public_keys'],
1302
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1303
                    $this->network,
1304
                    $this->testnet,
1305
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1306
                    $addressReader,
1307
                    $data['checksum']
1308
                );
1309
                break;
1310 15
            case Wallet::WALLET_VERSION_V3:
1311 15
                if (isset($options['encrypted_primary_seed'])) {
1312
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1313
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1314
                    }
1315
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1316
                } else {
1317 15
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1318
                }
1319
1320 15
                if (isset($options['encrypted_secret'])) {
1321
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1322
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1323
                    }
1324
1325
                    $encryptedSecret = $data['encrypted_secret'];
1326
                } else {
1327 15
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1328
                }
1329
1330 15
                $wallet = new WalletV3(
1331 15
                    $this,
1332 15
                    $identifier,
1333 15
                    $encryptedPrimarySeed,
1334 15
                    $encryptedSecret,
1335 15
                    $data['primary_public_keys'],
1336 15
                    $data['backup_public_key'],
1337 15
                    $data['blocktrail_public_keys'],
1338 15
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1339 15
                    $this->network,
1340 15
                    $this->testnet,
1341 15
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1342 15
                    $addressReader,
1343 15
                    $data['checksum']
1344
                );
1345 15
                break;
1346
            default:
1347
                throw new \InvalidArgumentException("Invalid wallet version");
1348
        }
1349
1350 15
        if (!$readonly) {
1351 15
            $wallet->unlock($options);
1352
        }
1353
1354 15
        return $wallet;
1355
    }
1356
1357
    /**
1358
     * get the wallet data from the server
1359
     *
1360
     * @param string    $identifier             the identifier of the wallet
1361
     * @return mixed
1362
     */
1363
    public function getWallet($identifier) {
1364
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1365
        return self::jsonDecode($response->body(), true);
1366
    }
1367
1368
    /**
1369
     * update the wallet data on the server
1370
     *
1371
     * @param string    $identifier
1372
     * @param $data
1373
     * @return mixed
1374
     */
1375
    public function updateWallet($identifier, $data) {
1376
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1377
        return self::jsonDecode($response->body(), true);
1378
    }
1379
1380
    /**
1381
     * delete a wallet from the server
1382
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1383
     *  is required to be able to delete a wallet
1384
     *
1385
     * @param string    $identifier             the identifier of the wallet
1386
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1387
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1388
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1389
     * @return mixed
1390
     */
1391
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1392
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1393
            'checksum' => $checksumAddress,
1394
            'signature' => $signature
1395
        ], RestClient::AUTH_HTTP_SIG, 360);
1396
        return self::jsonDecode($response->body(), true);
1397
    }
1398
1399
    /**
1400
     * create new backup key;
1401
     *  1) a BIP39 mnemonic
1402
     *  2) a seed from that mnemonic with a blank password
1403
     *  3) a private key from that seed
1404
     *
1405
     * @return array [mnemonic, seed, key]
1406
     */
1407 1
    protected function newV1BackupSeed() {
1408 1
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1409
1410 1
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1411
    }
1412
1413
    /**
1414
     * create new primary key;
1415
     *  1) a BIP39 mnemonic
1416
     *  2) a seed from that mnemonic with the password
1417
     *  3) a private key from that seed
1418
     *
1419
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1420
     * @return array [mnemonic, seed, key]
1421
     * @TODO: require a strong password?
1422
     */
1423 1
    protected function newV1PrimarySeed($passphrase) {
1424 1
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1425
1426 1
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1427
    }
1428
1429
    /**
1430
     * create a new key;
1431
     *  1) a BIP39 mnemonic
1432
     *  2) a seed from that mnemonic with the password
1433
     *  3) a private key from that seed
1434
     *
1435
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1436
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1437
     * @return array
1438
     */
1439 1
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1440
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1441
        do {
1442 1
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1443
1444 1
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1445
1446 1
            $key = null;
1447
            try {
1448 1
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1449
            } catch (\Exception $e) {
1450
                // try again
1451
            }
1452 1
        } while (!$key);
1453
1454 1
        return [$mnemonic, $seed, $key];
1455
    }
1456
1457
    /**
1458
     * generate a new mnemonic from some random entropy (512 bit)
1459
     *
1460
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1461
     * @return string
1462
     * @throws \Exception
1463
     */
1464
    public function generateNewMnemonic($forceEntropy = null) {
1465
        if ($forceEntropy === null) {
1466
            $random = new Random();
1467
            $entropy = $random->bytes(512 / 8);
1468
        } else {
1469
            $entropy = $forceEntropy;
1470
        }
1471
1472
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1469 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...
1473
    }
1474
1475
    /**
1476
     * get the balance for the wallet
1477
     *
1478
     * @param string    $identifier             the identifier of the wallet
1479
     * @return array
1480
     */
1481
    public function getWalletBalance($identifier) {
1482
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1483
        return self::jsonDecode($response->body(), true);
1484
    }
1485
1486
    /**
1487
     * get a new derivation number for specified parent path
1488
     *  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
1489
     *
1490
     * returns the path
1491
     *
1492
     * @param string    $identifier             the identifier of the wallet
1493
     * @param string    $path                   the parent path for which to get a new derivation
1494
     * @return string
1495
     */
1496
    public function getNewDerivation($identifier, $path) {
1497
        $result = $this->_getNewDerivation($identifier, $path);
1498
        return $result['path'];
1499
    }
1500
1501
    /**
1502
     * get a new derivation number for specified parent path
1503
     *  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
1504
     *
1505
     * @param string    $identifier             the identifier of the wallet
1506
     * @param string    $path                   the parent path for which to get a new derivation
1507
     * @return mixed
1508
     */
1509
    public function _getNewDerivation($identifier, $path) {
1510
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1511
        return self::jsonDecode($response->body(), true);
1512
    }
1513
1514
    /**
1515
     * get the path (and redeemScript) to specified address
1516
     *
1517
     * @param string $identifier
1518
     * @param string $address
1519
     * @return array
1520
     * @throws \Exception
1521
     */
1522
    public function getPathForAddress($identifier, $address) {
1523
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1524
        return self::jsonDecode($response->body(), true)['path'];
1525
    }
1526
1527
    /**
1528
     * send the transaction using the API
1529
     *
1530
     * @param string       $identifier     the identifier of the wallet
1531
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1532
     * @param array        $paths          list of the paths that were used for the UTXO
1533
     * @param bool         $checkFee       let the server verify the fee after signing
1534
     * @param null         $twoFactorToken
1535
     * @return string                                the complete raw transaction
1536
     * @throws \Exception
1537
     */
1538
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1539
        $data = [
1540
            'paths' => $paths,
1541
            'two_factor_token' => $twoFactorToken,
1542
        ];
1543
1544
        if (is_array($rawTransaction)) {
1545
            if (array_key_exists('base_transaction', $rawTransaction)
1546
            && array_key_exists('signed_transaction', $rawTransaction)) {
1547
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1548
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1549
            } else {
1550
                throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1551
            }
1552
        } else {
1553
            $data['raw_transaction'] = $rawTransaction;
1554
        }
1555
1556
        // dynamic TTL for when we're signing really big transactions
1557
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1558
1559
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1560
        $signed = self::jsonDecode($response->body(), true);
1561
1562
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1563
            throw new \Exception("Failed to completely sign transaction");
1564
        }
1565
1566
        // create TX hash from the raw signed hex
1567
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1568
    }
1569
1570
    /**
1571
     * use the API to get the best inputs to use based on the outputs
1572
     *
1573
     * the return array has the following format:
1574
     * [
1575
     *  "utxos" => [
1576
     *      [
1577
     *          "hash" => "<txHash>",
1578
     *          "idx" => "<index of the output of that <txHash>",
1579
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1580
     *          "value" => 32746327,
1581
     *          "address" => "1address",
1582
     *          "path" => "m/44'/1'/0'/0/13",
1583
     *          "redeem_script" => "<redeemScript-hex>",
1584
     *      ],
1585
     *  ],
1586
     *  "fee"   => 10000,
1587
     *  "change"=> 1010109201,
1588
     * ]
1589
     *
1590
     * @param string   $identifier              the identifier of the wallet
1591
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1592
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1593
     *                                          so you have some time to spend them without race-conditions
1594
     * @param bool     $allowZeroConf
1595
     * @param string   $feeStrategy
1596
     * @param null|int $forceFee
1597
     * @return array
1598
     * @throws \Exception
1599
     */
1600
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1601
        $args = [
1602
            'lock' => (int)!!$lockUTXO,
1603
            'zeroconf' => (int)!!$allowZeroConf,
1604
            'fee_strategy' => $feeStrategy,
1605
        ];
1606
1607
        if ($forceFee !== null) {
1608
            $args['forcefee'] = (int)$forceFee;
1609
        }
1610
1611
        $response = $this->blocktrailClient->post(
1612
            "wallet/{$identifier}/coin-selection",
1613
            $args,
1614
            $outputs,
1615
            RestClient::AUTH_HTTP_SIG
1616
        );
1617
1618
        \var_export(self::jsonDecode($response->body(), true));
1619
1620
        return self::jsonDecode($response->body(), true);
1621
    }
1622
1623
    /**
1624
     *
1625
     * @param string   $identifier the identifier of the wallet
1626
     * @param bool     $allowZeroConf
1627
     * @param string   $feeStrategy
1628
     * @param null|int $forceFee
1629
     * @param int      $outputCnt
1630
     * @return array
1631
     * @throws \Exception
1632
     */
1633
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1634
        $args = [
1635
            'zeroconf' => (int)!!$allowZeroConf,
1636
            'fee_strategy' => $feeStrategy,
1637
            'outputs' => $outputCnt,
1638
        ];
1639
1640
        if ($forceFee !== null) {
1641
            $args['forcefee'] = (int)$forceFee;
1642
        }
1643
1644
        $response = $this->blocktrailClient->get(
1645
            "wallet/{$identifier}/max-spendable",
1646
            $args,
1647
            RestClient::AUTH_HTTP_SIG
1648
        );
1649
1650
        return self::jsonDecode($response->body(), true);
1651
    }
1652
1653
    /**
1654
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1655
     */
1656
    public function feePerKB() {
1657
        $response = $this->blocktrailClient->get("fee-per-kb");
1658
        return self::jsonDecode($response->body(), true);
1659
    }
1660
1661
    /**
1662
     * get the current price index
1663
     *
1664
     * @return array        eg; ['USD' => 287.30]
1665
     */
1666
    public function price() {
1667
        $response = $this->blocktrailClient->get("price");
1668
        return self::jsonDecode($response->body(), true);
1669
    }
1670
1671
    /**
1672
     * setup webhook for wallet
1673
     *
1674
     * @param string    $identifier         the wallet identifier for which to create the webhook
1675
     * @param string    $webhookIdentifier  the webhook identifier to use
1676
     * @param string    $url                the url to receive the webhook events
1677
     * @return array
1678
     */
1679
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1680
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1681
        return self::jsonDecode($response->body(), true);
1682
    }
1683
1684
    /**
1685
     * delete webhook for wallet
1686
     *
1687
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1688
     * @param string    $webhookIdentifier  the webhook identifier to delete
1689
     * @return array
1690
     */
1691
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1692
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1693
        return self::jsonDecode($response->body(), true);
1694
    }
1695
1696
    /**
1697
     * lock a specific unspent output
1698
     *
1699
     * @param     $identifier
1700
     * @param     $txHash
1701
     * @param     $txIdx
1702
     * @param int $ttl
1703
     * @return bool
1704
     */
1705
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1706
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1707
        return self::jsonDecode($response->body(), true)['locked'];
1708
    }
1709
1710
    /**
1711
     * unlock a specific unspent output
1712
     *
1713
     * @param     $identifier
1714
     * @param     $txHash
1715
     * @param     $txIdx
1716
     * @return bool
1717
     */
1718
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1719
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1720
        return self::jsonDecode($response->body(), true)['unlocked'];
1721
    }
1722
1723
    /**
1724
     * get all transactions for wallet (paginated)
1725
     *
1726
     * @param  string  $identifier  the wallet identifier for which to get transactions
1727
     * @param  integer $page        pagination: page number
1728
     * @param  integer $limit       pagination: records per page (max 500)
1729
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1730
     * @return array                associative array containing the response
1731
     */
1732
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1733
        $queryString = [
1734
            'page' => $page,
1735
            'limit' => $limit,
1736
            'sort_dir' => $sortDir
1737
        ];
1738
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1739
        return self::jsonDecode($response->body(), true);
1740
    }
1741
1742
    /**
1743
     * get all addresses for wallet (paginated)
1744
     *
1745
     * @param  string  $identifier  the wallet identifier for which to get addresses
1746
     * @param  integer $page        pagination: page number
1747
     * @param  integer $limit       pagination: records per page (max 500)
1748
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1749
     * @return array                associative array containing the response
1750
     */
1751
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1752
        $queryString = [
1753
            'page' => $page,
1754
            'limit' => $limit,
1755
            'sort_dir' => $sortDir
1756
        ];
1757
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1758
        return self::jsonDecode($response->body(), true);
1759
    }
1760
1761
    /**
1762
     * get all UTXOs for wallet (paginated)
1763
     *
1764
     * @param  string  $identifier  the wallet identifier for which to get addresses
1765
     * @param  integer $page        pagination: page number
1766
     * @param  integer $limit       pagination: records per page (max 500)
1767
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1768
     * @param  boolean $zeroconf    include zero confirmation transactions
1769
     * @return array                associative array containing the response
1770
     */
1771
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1772
        $queryString = [
1773
            'page' => $page,
1774
            'limit' => $limit,
1775
            'sort_dir' => $sortDir,
1776
            'zeroconf' => (int)!!$zeroconf,
1777
        ];
1778
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1779
        return self::jsonDecode($response->body(), true);
1780
    }
1781
1782
    /**
1783
     * get a paginated list of all wallets associated with the api user
1784
     *
1785
     * @param  integer          $page    pagination: page number
1786
     * @param  integer          $limit   pagination: records per page
1787
     * @return array                     associative array containing the response
1788
     */
1789
    public function allWallets($page = 1, $limit = 20) {
1790
        $queryString = [
1791
            'page' => $page,
1792
            'limit' => $limit
1793
        ];
1794
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1795
        return self::jsonDecode($response->body(), true);
1796
    }
1797
1798
    /**
1799
     * send raw transaction
1800
     *
1801
     * @param     $txHex
1802
     * @return bool
1803
     */
1804
    public function sendRawTransaction($txHex) {
1805
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1806
        return self::jsonDecode($response->body(), true);
1807
    }
1808
1809
    /**
1810
     * testnet only ;-)
1811
     *
1812
     * @param     $address
1813
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1814
     * @return mixed
1815
     * @throws \Exception
1816
     */
1817
    public function faucetWithdrawal($address, $amount = 10000) {
1818
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1819
            'address' => $address,
1820
            'amount' => $amount,
1821
        ], RestClient::AUTH_HTTP_SIG);
1822
        return self::jsonDecode($response->body(), true);
1823
    }
1824
1825
    /**
1826
     * Exists for BC. Remove at major bump.
1827
     *
1828
     * @see faucetWithdrawal
1829
     * @deprecated
1830
     * @param     $address
1831
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1832
     * @return mixed
1833
     * @throws \Exception
1834
     */
1835
    public function faucetWithdrawl($address, $amount = 10000) {
1836
        return $this->faucetWithdrawal($address, $amount);
1837
    }
1838
1839
    /**
1840
     * verify a message signed bitcoin-core style
1841
     *
1842
     * @param  string           $message
1843
     * @param  string           $address
1844
     * @param  string           $signature
1845
     * @return boolean
1846
     */
1847
    public function verifyMessage($message, $address, $signature) {
1848
        $adapter = Bitcoin::getEcAdapter();
1849
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1850
        if (!$addr instanceof PayToPubKeyHashAddress) {
1851
            throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
1852
        }
1853
1854
        /** @var CompactSignatureSerializerInterface $csSerializer */
1855
        $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...
1856
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1857
1858
        $signer = new MessageSigner($adapter);
1859
        return $signer->verify($signedMessage, $addr);
1860
    }
1861
1862
    /**
1863
     * Take a base58 or cashaddress, and return only
1864
     * the cash address.
1865
     * This function only works on bitcoin cash.
1866
     * @param string $input
1867
     * @return string
1868
     * @throws BlocktrailSDKException
1869
     */
1870
    public function getLegacyBitcoinCashAddress($input) {
1871
        if ($this->network === "bitcoincash") {
1872
            $address = $this
1873
                ->makeAddressReader([
1874
                    "use_cashaddress" => true
1875
                ])
1876
                ->fromString($input);
1877
1878
            if ($address instanceof CashAddress) {
1879
                $address = $address->getLegacyAddress();
1880
            }
1881
1882
            return $address->getAddress();
1883
        }
1884
1885
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1886
    }
1887
1888
    /**
1889
     * convert a Satoshi value to a BTC value
1890
     *
1891
     * @param int       $satoshi
1892
     * @return float
1893
     */
1894
    public static function toBTC($satoshi) {
1895
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1896
    }
1897
1898
    /**
1899
     * convert a Satoshi value to a BTC value and return it as a string
1900
1901
     * @param int       $satoshi
1902
     * @return string
1903
     */
1904
    public static function toBTCString($satoshi) {
1905
        return sprintf("%.8f", self::toBTC($satoshi));
1906
    }
1907
1908
    /**
1909
     * convert a BTC value to a Satoshi value
1910
     *
1911
     * @param float     $btc
1912
     * @return string
1913
     */
1914 7
    public static function toSatoshiString($btc) {
1915 7
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1916
    }
1917
1918
    /**
1919
     * convert a BTC value to a Satoshi value
1920
     *
1921
     * @param float     $btc
1922
     * @return string
1923
     */
1924 7
    public static function toSatoshi($btc) {
1925 7
        return (int)self::toSatoshiString($btc);
1926
    }
1927
1928
    /**
1929
     * json_decode helper that throws exceptions when it fails to decode
1930
     *
1931
     * @param      $json
1932
     * @param bool $assoc
1933
     * @return mixed
1934
     * @throws \Exception
1935
     */
1936
    public static function jsonDecode($json, $assoc = false) {
1937
        if (!$json) {
1938
            throw new \Exception("Can't json_decode empty string [{$json}]");
1939
        }
1940
1941
        $data = json_decode($json, $assoc);
1942
1943
        if ($data === null) {
1944
            throw new \Exception("Failed to json_decode [{$json}]");
1945
        }
1946
1947
        return $data;
1948
    }
1949
1950
    /**
1951
     * sort public keys for multisig script
1952
     *
1953
     * @param PublicKeyInterface[] $pubKeys
1954
     * @return PublicKeyInterface[]
1955
     */
1956 13
    public static function sortMultisigKeys(array $pubKeys) {
1957 13
        $result = array_values($pubKeys);
1958 13
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1959 13
            $av = $a->getHex();
1960 13
            $bv = $b->getHex();
1961 13
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1962 13
        });
1963
1964 13
        return $result;
1965
    }
1966
1967
    /**
1968
     * read and decode the json payload from a webhook's POST request.
1969
     *
1970
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1971
     * @return mixed|null
1972
     * @throws \Exception
1973
     */
1974
    public static function getWebhookPayload($returnObject = false) {
1975
        $data = file_get_contents("php://input");
1976
        if ($data) {
1977
            return self::jsonDecode($data, !$returnObject);
1978
        } else {
1979
            return null;
1980
        }
1981
    }
1982
1983
    public static function normalizeBIP32KeyArray($keys) {
1984 17
        return Util::arrayMapWithIndex(function ($idx, $key) {
1985 17
            return [$idx, self::normalizeBIP32Key($key)];
1986 17
        }, $keys);
1987
    }
1988
1989
    /**
1990
     * @param array|BIP32Key $key
1991
     * @return BIP32Key
1992
     * @throws \Exception
1993
     */
1994 17
    public static function normalizeBIP32Key($key) {
1995 17
        if ($key instanceof BIP32Key) {
1996 2
            return $key;
1997
        }
1998
1999 15
        if (is_array($key) && count($key) === 2) {
2000 15
            $path = $key[1];
2001 15
            $hk = $key[0];
2002
2003 15
            if (!($hk instanceof HierarchicalKey)) {
2004 15
                $hk = HierarchicalKeyFactory::fromExtended($hk);
2005
            }
2006
2007 15
            return BIP32Key::create($hk, $path);
2008
        } else {
2009
            throw new \Exception("Bad Input");
2010
        }
2011
    }
2012
2013 1
    public function shuffle($arr) {
2014 1
        \shuffle($arr);
2015 1
    }
2016
}
2017