Completed
Pull Request — master (#125)
by thomas
75:30 queued 72:15
created

BlocktrailSDK::addressTransactions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
151 17
    }
152
153
    /**
154
     * enable CURL debugging output
155
     *
156
     * @param   bool        $debug
157
     *
158
     * @codeCoverageIgnore
159
     */
160
    public function setCurlDebugging($debug = true) {
161
        $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...
162
        $this->dataClient->setCurlDebugging($debug);
163
    }
164
165
    /**
166
     * enable verbose errors
167
     *
168
     * @param   bool        $verboseErrors
169
     *
170
     * @codeCoverageIgnore
171
     */
172
    public function setVerboseErrors($verboseErrors = true) {
173
        $this->blocktrailClient->setVerboseErrors($verboseErrors);
174
        $this->dataClient->setVerboseErrors($verboseErrors);
175
    }
176
    
177
    /**
178
     * set cURL default option on Guzzle client
179
     * @param string    $key
180
     * @param bool      $value
181
     *
182
     * @codeCoverageIgnore
183
     */
184
    public function setCurlDefaultOption($key, $value) {
185
        $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...
186
        $this->dataClient->setCurlDefaultOption($key, $value);
187
    }
188
189
    /**
190
     * @return  RestClientInterface
191
     */
192 1
    public function getRestClient() {
193 1
        return $this->blocktrailClient;
194
    }
195
196
    /**
197
     * @return  RestClient
198
     */
199
    public function getDataRestClient() {
200
        return $this->dataClient;
201
    }
202
203
    /**
204
     * @param RestClientInterface $restClient
205
     */
206
    public function setRestClient(RestClientInterface $restClient) {
207
        $this->blocktrailClient = $restClient;
208
    }
209
210
    /**
211
     * get a single address
212
     * @param  string $address address hash
213
     * @return array           associative array containing the response
214
     */
215 2
    public function address($address) {
216 2
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
217 2
        return $this->converter->convertAddress($response->body());
218
    }
219
220
    /**
221
     * get all transactions for an address (paginated)
222
     * @param  string  $address address hash
223
     * @param  integer $page    pagination: page number
224
     * @param  integer $limit   pagination: records per page (max 500)
225
     * @param  string  $sortDir pagination: sort direction (asc|desc)
226
     * @return array            associative array containing the response
227
     */
228 2
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
229
        $queryString = [
230 2
            'page' => $page,
231 2
            'limit' => $limit,
232 2
            'sort_dir' => $sortDir,
233
        ];
234 2
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
235 2
        return $this->converter->convertAddressTxs($response->body());
236
    }
237
238
    /**
239
     * get all unconfirmed transactions for an address (paginated)
240
     * @param  string  $address address hash
241
     * @param  integer $page    pagination: page number
242
     * @param  integer $limit   pagination: records per page (max 500)
243
     * @param  string  $sortDir pagination: sort direction (asc|desc)
244
     * @return array            associative array containing the response
245
     */
246
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
247
        $queryString = [
248
            'page' => $page,
249
            'limit' => $limit,
250
            'sort_dir' => $sortDir
251
        ];
252
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
253
        return $this->converter->convertAddressTxs($response->body());
254
    }
255
256
    /**
257
     * get all unspent outputs for an address (paginated)
258
     * @param  string  $address address hash
259
     * @param  integer $page    pagination: page number
260
     * @param  integer $limit   pagination: records per page (max 500)
261
     * @param  string  $sortDir pagination: sort direction (asc|desc)
262
     * @return array            associative array containing the response
263
     */
264 2
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
265
        $queryString = [
266 2
            'page' => $page,
267 2
            'limit' => $limit,
268 2
            'sort_dir' => $sortDir
269
        ];
270 2
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
271 2
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
272
    }
273
274
    /**
275
     * get all unspent outputs for a batch of addresses (paginated)
276
     *
277
     * @param  string[] $addresses
278
     * @param  integer  $page    pagination: page number
279
     * @param  integer  $limit   pagination: records per page (max 500)
280
     * @param  string   $sortDir pagination: sort direction (asc|desc)
281
     * @return array associative array containing the response
282
     * @throws \Exception
283
     */
284
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
285
        $queryString = [
286
            'page' => $page,
287
            'limit' => $limit,
288
            'sort_dir' => $sortDir
289
        ];
290
291
        if ($this->converter instanceof BtccomConverter) {
292
            if ($page > 1) {
293
                return [
294
                    'data' => [],
295
                    'current_page' => 2,
296
                    'per_page' => null,
297
                    'total' => null,
298
                ];
299
            }
300
301
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
302
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
303
        } else {
304
            $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...
305
            return self::jsonDecode($response->body(), true);
306
        }
307
    }
308
309
    /**
310
     * verify ownership of an address
311
     * @param  string  $address     address hash
312
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
313
     * @return array                associative array containing the response
314
     */
315 2
    public function verifyAddress($address, $signature) {
316 2
        if ($this->verifyMessage($address, $address, $signature)) {
317 2
            return ['result' => true, 'msg' => 'Successfully verified'];
318
        } else {
319
            return ['result' => false];
320
        }
321
    }
322
323
    /**
324
     * get all blocks (paginated)
325
     * @param  integer $page    pagination: page number
326
     * @param  integer $limit   pagination: records per page
327
     * @param  string  $sortDir pagination: sort direction (asc|desc)
328
     * @return array            associative array containing the response
329
     */
330 2
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
331
        $queryString = [
332 2
            'page' => $page,
333 2
            'limit' => $limit,
334 2
            'sort_dir' => $sortDir
335
        ];
336 2
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
337 2
        return $this->converter->convertBlocks($response->body());
338
    }
339
340
    /**
341
     * get the latest block
342
     * @return array            associative array containing the response
343
     */
344 1
    public function blockLatest() {
345 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
346 1
        return $this->converter->convertBlock($response->body());
347
    }
348
349
    /**
350
     * get an individual block
351
     * @param  string|integer $block    a block hash or a block height
352
     * @return array                    associative array containing the response
353
     */
354 4
    public function block($block) {
355 4
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
356 4
        return $this->converter->convertBlock($response->body());
357
    }
358
359
    /**
360
     * get all transaction in a block (paginated)
361
     * @param  string|integer   $block   a block hash or a block height
362
     * @param  integer          $page    pagination: page number
363
     * @param  integer          $limit   pagination: records per page
364
     * @param  string           $sortDir pagination: sort direction (asc|desc)
365
     * @return array                     associative array containing the response
366
     */
367 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
368
        $queryString = [
369 1
            'page' => $page,
370 1
            'limit' => $limit,
371 1
            'sort_dir' => $sortDir
372
        ];
373 1
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
374 1
        return $this->converter->convertBlockTxs($response->body());
375
    }
376
377
    /**
378
     * get a single transaction
379
     * @param  string $txhash transaction hash
380
     * @return array          associative array containing the response
381
     */
382 2
    public function transaction($txhash) {
383 2
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
384 2
        $res = $this->converter->convertTx($response->body(), null);
385
386 2
        if ($this->converter instanceof BtccomConverter) {
387 1
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
388
        }
389
390 2
        return $res;
391
    }
392
393
    /**
394
     * get a single transaction
395
     * @param  string[] $txhashes list of transaction hashes (up to 20)
396
     * @return array[]            array containing the response
397
     */
398 2
    public function transactions($txhashes) {
399 2
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
400 2
        return $this->converter->convertTxs($response->body());
401
    }
402
    
403
    /**
404
     * get a paginated list of all webhooks associated with the api user
405
     * @param  integer          $page    pagination: page number
406
     * @param  integer          $limit   pagination: records per page
407
     * @return array                     associative array containing the response
408
     */
409
    public function allWebhooks($page = 1, $limit = 20) {
410
        $queryString = [
411
            'page' => $page,
412
            'limit' => $limit
413
        ];
414
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
415
        return self::jsonDecode($response->body(), true);
416
    }
417
418
    /**
419
     * get an existing webhook by it's identifier
420
     * @param string    $identifier     a unique identifier associated with the webhook
421
     * @return array                    associative array containing the response
422
     */
423
    public function getWebhook($identifier) {
424
        $response = $this->blocktrailClient->get("webhook/".$identifier);
425
        return self::jsonDecode($response->body(), true);
426
    }
427
428
    /**
429
     * create a new webhook
430
     * @param  string  $url        the url to receive the webhook events
431
     * @param  string  $identifier a unique identifier to associate with this webhook
432
     * @return array               associative array containing the response
433
     */
434
    public function setupWebhook($url, $identifier = null) {
435
        $postData = [
436
            'url'        => $url,
437
            'identifier' => $identifier
438
        ];
439
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
440
        return self::jsonDecode($response->body(), true);
441
    }
442
443
    /**
444
     * update an existing webhook
445
     * @param  string  $identifier      the unique identifier of the webhook to update
446
     * @param  string  $newUrl          the new url to receive the webhook events
447
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
448
     * @return array                    associative array containing the response
449
     */
450
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
451
        $putData = [
452
            'url'        => $newUrl,
453
            'identifier' => $newIdentifier
454
        ];
455
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
456
        return self::jsonDecode($response->body(), true);
457
    }
458
459
    /**
460
     * deletes an existing webhook and any event subscriptions associated with it
461
     * @param  string  $identifier      the unique identifier of the webhook to delete
462
     * @return boolean                  true on success
463
     */
464
    public function deleteWebhook($identifier) {
465
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
466
        return self::jsonDecode($response->body(), true);
467
    }
468
469
    /**
470
     * get a paginated list of all the events a webhook is subscribed to
471
     * @param  string  $identifier  the unique identifier of the webhook
472
     * @param  integer $page        pagination: page number
473
     * @param  integer $limit       pagination: records per page
474
     * @return array                associative array containing the response
475
     */
476
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
477
        $queryString = [
478
            'page' => $page,
479
            'limit' => $limit
480
        ];
481
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
482
        return self::jsonDecode($response->body(), true);
483
    }
484
    
485
    /**
486
     * subscribes a webhook to transaction events of one particular transaction
487
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
488
     * @param  string  $transaction     the transaction hash
489
     * @param  integer $confirmations   the amount of confirmations to send.
490
     * @return array                    associative array containing the response
491
     */
492
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
493
        $postData = [
494
            'event_type'    => 'transaction',
495
            'transaction'   => $transaction,
496
            'confirmations' => $confirmations,
497
        ];
498
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
499
        return self::jsonDecode($response->body(), true);
500
    }
501
502
    /**
503
     * subscribes a webhook to transaction events on a particular address
504
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
505
     * @param  string  $address         the address hash
506
     * @param  integer $confirmations   the amount of confirmations to send.
507
     * @return array                    associative array containing the response
508
     */
509
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
510
        $postData = [
511
            'event_type'    => 'address-transactions',
512
            'address'       => $address,
513
            'confirmations' => $confirmations,
514
        ];
515
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
516
        return self::jsonDecode($response->body(), true);
517
    }
518
519
    /**
520
     * batch subscribes a webhook to multiple transaction events
521
     *
522
     * @param  string $identifier   the unique identifier of the webhook
523
     * @param  array  $batchData    A 2D array of event data:
524
     *                              [address => $address, confirmations => $confirmations]
525
     *                              where $address is the address to subscibe to
526
     *                              and optionally $confirmations is the amount of confirmations
527
     * @return boolean              true on success
528
     */
529
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
530
        $postData = [];
531
        foreach ($batchData as $record) {
532
            $postData[] = [
533
                'event_type' => 'address-transactions',
534
                'address' => $record['address'],
535
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
536
            ];
537
        }
538
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
539
        return self::jsonDecode($response->body(), true);
540
    }
541
542
    /**
543
     * subscribes a webhook to a new block event
544
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
545
     * @return array                associative array containing the response
546
     */
547
    public function subscribeNewBlocks($identifier) {
548
        $postData = [
549
            'event_type'    => 'block',
550
        ];
551
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
552
        return self::jsonDecode($response->body(), true);
553
    }
554
555
    /**
556
     * removes an transaction event subscription from a webhook
557
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
558
     * @param  string  $transaction     the transaction hash of the event subscription
559
     * @return boolean                  true on success
560
     */
561
    public function unsubscribeTransaction($identifier, $transaction) {
562
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
563
        return self::jsonDecode($response->body(), true);
564
    }
565
566
    /**
567
     * removes an address transaction event subscription from a webhook
568
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
569
     * @param  string  $address         the address hash of the event subscription
570
     * @return boolean                  true on success
571
     */
572
    public function unsubscribeAddressTransactions($identifier, $address) {
573
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
574
        return self::jsonDecode($response->body(), true);
575
    }
576
577
    /**
578
     * removes a block event subscription from a webhook
579
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
580
     * @return boolean                  true on success
581
     */
582
    public function unsubscribeNewBlocks($identifier) {
583
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
584
        return self::jsonDecode($response->body(), true);
585
    }
586
587
    /**
588
     * create a new wallet
589
     *   - will generate a new primary seed (with password) and backup seed (without password)
590
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
591
     *   - receive the blocktrail co-signing public key from the server
592
     *
593
     * Either takes one argument:
594
     * @param array $options
595
     *
596
     * Or takes three arguments (old, deprecated syntax):
597
     * (@nonPHP-doc) @param      $identifier
598
     * (@nonPHP-doc) @param      $password
599
     * (@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...
600
     *
601
     * @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...
602
     * @throws \Exception
603
     */
604
    public function createNewWallet($options) {
605
        if (!is_array($options)) {
606
            $args = func_get_args();
607
            $options = [
608
                "identifier" => $args[0],
609
                "password" => $args[1],
610
                "key_index" => isset($args[2]) ? $args[2] : null,
611
            ];
612
        }
613
614
        if (isset($options['password'])) {
615
            if (isset($options['passphrase'])) {
616
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
617
            } else {
618
                $options['passphrase'] = $options['password'];
619
            }
620
        }
621
622
        if (!isset($options['passphrase'])) {
623
            $options['passphrase'] = null;
624
        }
625
626
        if (!isset($options['key_index'])) {
627
            $options['key_index'] = 0;
628
        }
629
630
        if (!isset($options['wallet_version'])) {
631
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
632
        }
633
634
        switch ($options['wallet_version']) {
635
            case Wallet::WALLET_VERSION_V1:
636
                return $this->createNewWalletV1($options);
637
638
            case Wallet::WALLET_VERSION_V2:
639
                return $this->createNewWalletV2($options);
640
641
            case Wallet::WALLET_VERSION_V3:
642
                return $this->createNewWalletV3($options);
643
644
            default:
645
                throw new \InvalidArgumentException("Invalid wallet version");
646
        }
647
    }
648
649
    protected function createNewWalletV1($options) {
650
        $walletPath = WalletPath::create($options['key_index']);
651
652
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
653
654
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
655
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
656
        }
657
658
        $primaryMnemonic = null;
659
        $primaryPrivateKey = null;
660
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
661
            if (!$options['passphrase']) {
662
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
663
            } else {
664
                // create new primary seed
665
                /** @var HierarchicalKey $primaryPrivateKey */
666
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
667
                if ($storePrimaryMnemonic !== false) {
668
                    $storePrimaryMnemonic = true;
669
                }
670
            }
671
        } elseif (isset($options['primary_mnemonic'])) {
672
            $primaryMnemonic = $options['primary_mnemonic'];
673
        } elseif (isset($options['primary_private_key'])) {
674
            $primaryPrivateKey = $options['primary_private_key'];
675
        }
676
677
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
678
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
679
        }
680
681
        if ($primaryPrivateKey) {
682
            if (is_string($primaryPrivateKey)) {
683
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
684
            }
685
        } else {
686
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
687
        }
688
689
        if (!$storePrimaryMnemonic) {
690
            $primaryMnemonic = false;
691
        }
692
693
        // create primary public key from the created private key
694
        $path = $walletPath->keyIndexPath()->publicPath();
695
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
696
697
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
698
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
699
        }
700
701
        $backupMnemonic = null;
702
        $backupPublicKey = null;
703
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
704
            /** @var HierarchicalKey $backupPrivateKey */
705
            list($backupMnemonic, , ) = $this->newV1BackupSeed();
706
        } else if (isset($options['backup_mnemonic'])) {
707
            $backupMnemonic = $options['backup_mnemonic'];
708
        } elseif (isset($options['backup_public_key'])) {
709
            $backupPublicKey = $options['backup_public_key'];
710
        }
711
712
        if ($backupPublicKey) {
713
            if (is_string($backupPublicKey)) {
714
                $backupPublicKey = [$backupPublicKey, "m"];
715
            }
716
        } else {
717
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
718
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
719
        }
720
721
        // create a checksum of our private key which we'll later use to verify we used the right password
722
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
723
        $addressReader = $this->makeAddressReader($options);
724
725
        // send the public keys to the server to store them
726
        //  and the mnemonic, which is safe because it's useless without the password
727
        $data = $this->storeNewWalletV1(
728
            $options['identifier'],
729
            $primaryPublicKey->tuple(),
730
            $backupPublicKey->tuple(),
731
            $primaryMnemonic,
732
            $checksum,
733
            $options['key_index'],
734
            array_key_exists('segwit', $options) ? $options['segwit'] : false
735
        );
736
737
        // received the blocktrail public keys
738
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
739
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
740
        }, $data['blocktrail_public_keys']);
741
742
        $wallet = new WalletV1(
743
            $this,
744
            $options['identifier'],
745
            $primaryMnemonic,
746
            [$options['key_index'] => $primaryPublicKey],
747
            $backupPublicKey,
748
            $blocktrailPublicKeys,
749
            $options['key_index'],
750
            $this->network,
751
            $this->testnet,
752
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
753
            $addressReader,
754
            $checksum
755
        );
756
757
        $wallet->unlock($options);
758
759
        // return wallet and backup mnemonic
760
        return [
761
            $wallet,
762
            [
763
                'primary_mnemonic' => $primaryMnemonic,
764
                'backup_mnemonic' => $backupMnemonic,
765
                'blocktrail_public_keys' => $blocktrailPublicKeys,
766
            ],
767
        ];
768
    }
769
770
    public function randomBits($bits) {
771
        return $this->randomBytes($bits / 8);
772
    }
773
774
    public function randomBytes($bytes) {
775
        return (new Random())->bytes($bytes)->getBinary();
776
    }
777
778
    protected function createNewWalletV2($options) {
779
        $walletPath = WalletPath::create($options['key_index']);
780
781
        if (isset($options['store_primary_mnemonic'])) {
782
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
783
        }
784
785
        if (!isset($options['store_data_on_server'])) {
786
            if (isset($options['primary_private_key'])) {
787
                $options['store_data_on_server'] = false;
788
            } else {
789
                $options['store_data_on_server'] = true;
790
            }
791
        }
792
793
        $storeDataOnServer = $options['store_data_on_server'];
794
795
        $secret = null;
796
        $encryptedSecret = null;
797
        $primarySeed = null;
798
        $encryptedPrimarySeed = null;
799
        $recoverySecret = null;
800
        $recoveryEncryptedSecret = null;
801
        $backupSeed = null;
802
803
        if (!isset($options['primary_private_key'])) {
804
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed();
805
        }
806
807
        if ($storeDataOnServer) {
808
            if (!isset($options['secret'])) {
809
                if (!$options['passphrase']) {
810
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
811
                }
812
813
                list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']);
814
            } else {
815
                $secret = $options['secret'];
816
            }
817
818
            $encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret);
819
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret);
820
        }
821
822
        if (!isset($options['backup_public_key'])) {
823
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed();
824
        }
825
826
        if (isset($options['primary_private_key'])) {
827
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
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
    protected function createNewWalletV3($options) {
900
        $walletPath = WalletPath::create($options['key_index']);
901
902
        if (isset($options['store_primary_mnemonic'])) {
903
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
904
        }
905
906
        if (!isset($options['store_data_on_server'])) {
907
            if (isset($options['primary_private_key'])) {
908
                $options['store_data_on_server'] = false;
909
            } else {
910
                $options['store_data_on_server'] = true;
911
            }
912
        }
913
914
        $storeDataOnServer = $options['store_data_on_server'];
915
916
        $secret = null;
917
        $encryptedSecret = null;
918
        $primarySeed = null;
919
        $encryptedPrimarySeed = null;
920
        $recoverySecret = null;
921
        $recoveryEncryptedSecret = null;
922
        $backupSeed = null;
923
924
        if (!isset($options['primary_private_key'])) {
925
            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
                $primarySeed = $this->newV3PrimarySeed();
932
            }
933
        }
934
935
        if ($storeDataOnServer) {
936
            if (!isset($options['secret'])) {
937
                if (!$options['passphrase']) {
938
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
939
                }
940
941
                list($secret, $encryptedSecret) = $this->newV3Secret($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
            $encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret);
951
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret);
952
        }
953
954
        if (!isset($options['backup_public_key'])) {
955
            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
                $backupSeed = $this->newV3BackupSeed();
962
            }
963
        }
964
965
        if (isset($options['primary_private_key'])) {
966
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
967
        } else {
968
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
969
        }
970
971
        // create primary public key from the created private key
972
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
973
974
        if (!isset($options['backup_public_key'])) {
975
            $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
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
980
        $addressReader = $this->makeAddressReader($options);
981
982
        // send the public keys and encrypted data to server
983
        $data = $this->storeNewWalletV3(
984
            $options['identifier'],
985
            $options['primary_public_key']->tuple(),
986
            $options['backup_public_key']->tuple(),
987
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
988
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
989
            $storeDataOnServer ? $recoverySecret->getHex() : false,
990
            $checksum,
991
            $options['key_index'],
992
            array_key_exists('segwit', $options) ? $options['segwit'] : false
993
        );
994
995
        // received the blocktrail public keys
996
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
997
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
998
        }, $data['blocktrail_public_keys']);
999
1000
        $wallet = new WalletV3(
1001
            $this,
1002
            $options['identifier'],
1003
            $encryptedPrimarySeed,
1004
            $encryptedSecret,
1005
            [$options['key_index'] => $options['primary_public_key']],
1006
            $options['backup_public_key'],
1007
            $blocktrailPublicKeys,
1008
            $options['key_index'],
1009
            $this->network,
1010
            $this->testnet,
1011
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1012
            $addressReader,
1013
            $checksum
1014
        );
1015
1016
        $wallet->unlock([
1017
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1018
            'primary_private_key' => $options['primary_private_key'],
1019
            'primary_seed' => $primarySeed,
1020
            'secret' => $secret,
1021
        ]);
1022
1023
        // return wallet and mnemonics for backup sheet
1024
        return [
1025
            $wallet,
1026
            [
1027
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1028
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1029
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1030
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1031
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1032
                    return [$keyIndex, $pubKey->tuple()];
1033
                }, $blocktrailPublicKeys),
1034
            ]
1035
        ];
1036
    }
1037
1038
    public function newV2PrimarySeed() {
1039
        return $this->randomBits(256);
1040
    }
1041
1042
    public function newV2BackupSeed() {
1043
        return $this->randomBits(256);
1044
    }
1045
1046
    public function newV2Secret($passphrase) {
1047
        $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
1048
        $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase);
1049
1050
        return [$secret, $encryptedSecret];
1051
    }
1052
1053
    public function newV2EncryptedPrimarySeed($primarySeed, $secret) {
1054
        return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
1055
    }
1056
1057
    public function newV2RecoverySecret($secret) {
1058
        $recoverySecret = bin2hex($this->randomBits(256));
1059
        $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
1060
1061
        return [$recoverySecret, $recoveryEncryptedSecret];
1062
    }
1063
1064
    public function newV3PrimarySeed() {
1065
        return new Buffer($this->randomBits(256));
1066
    }
1067
1068
    public function newV3BackupSeed() {
1069
        return new Buffer($this->randomBits(256));
1070
    }
1071
1072
    public function newV3Secret($passphrase) {
1073
        $secret = new Buffer($this->randomBits(256));
1074
        $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
1075
            ->getBuffer();
1076
1077
        return [$secret, $encryptedSecret];
1078
    }
1079
1080
    public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
1081
        return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
1082
            ->getBuffer();
1083
    }
1084
1085
    public function newV3RecoverySecret(Buffer $secret) {
1086
        $recoverySecret = new Buffer($this->randomBits(256));
1087
        $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
1088
            ->getBuffer();
1089
1090
        return [$recoverySecret, $recoveryEncryptedSecret];
1091
    }
1092
1093
    /**
1094
     * @param array $bip32Key
1095
     * @throws BlocktrailSDKException
1096
     */
1097
    private function verifyPublicBIP32Key(array $bip32Key) {
1098
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1099
        if ($hk->isPrivate()) {
1100
            throw new BlocktrailSDKException('Private key was included in request, abort');
1101
        }
1102
1103
        if (substr($bip32Key[1], 0, 1) === "m") {
1104
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1105
        }
1106
    }
1107
1108
    /**
1109
     * @param array $walletData
1110
     * @throws BlocktrailSDKException
1111
     */
1112
    private function verifyPublicOnly(array $walletData) {
1113
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1114
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1115
    }
1116
1117
    /**
1118
     * create wallet using the API
1119
     *
1120
     * @param string    $identifier             the wallet identifier to create
1121
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1122
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1123
     * @param string    $primaryMnemonic        mnemonic to store
1124
     * @param string    $checksum               checksum to store
1125
     * @param int       $keyIndex               account that we expect to use
1126
     * @param bool      $segwit                 opt in to segwit
1127
     * @return mixed
1128
     */
1129
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1130
        $data = [
1131
            'identifier' => $identifier,
1132
            'primary_public_key' => $primaryPublicKey,
1133
            'backup_public_key' => $backupPublicKey,
1134
            'primary_mnemonic' => $primaryMnemonic,
1135
            'checksum' => $checksum,
1136
            'key_index' => $keyIndex,
1137
            'segwit' => $segwit,
1138
        ];
1139
        $this->verifyPublicOnly($data);
1140
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1141
        return self::jsonDecode($response->body(), true);
1142
    }
1143
1144
    /**
1145
     * create wallet using the API
1146
     *
1147
     * @param string $identifier       the wallet identifier to create
1148
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1149
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1150
     * @param        $encryptedPrimarySeed
1151
     * @param        $encryptedSecret
1152
     * @param        $recoverySecret
1153
     * @param string $checksum         checksum to store
1154
     * @param int    $keyIndex         account that we expect to use
1155
     * @param bool   $segwit           opt in to segwit
1156
     * @return mixed
1157
     * @throws \Exception
1158
     */
1159
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1160
        $data = [
1161
            'identifier' => $identifier,
1162
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1163
            'primary_public_key' => $primaryPublicKey,
1164
            'backup_public_key' => $backupPublicKey,
1165
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1166
            'encrypted_secret' => $encryptedSecret,
1167
            'recovery_secret' => $recoverySecret,
1168
            'checksum' => $checksum,
1169
            'key_index' => $keyIndex,
1170
            'segwit' => $segwit,
1171
        ];
1172
        $this->verifyPublicOnly($data);
1173
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1174
        return self::jsonDecode($response->body(), true);
1175
    }
1176
1177
    /**
1178
     * create wallet using the API
1179
     *
1180
     * @param string $identifier       the wallet identifier to create
1181
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1182
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1183
     * @param        $encryptedPrimarySeed
1184
     * @param        $encryptedSecret
1185
     * @param        $recoverySecret
1186
     * @param string $checksum         checksum to store
1187
     * @param int    $keyIndex         account that we expect to use
1188
     * @param bool   $segwit           opt in to segwit
1189
     * @return mixed
1190
     * @throws \Exception
1191
     */
1192
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1193
1194
        $data = [
1195
            'identifier' => $identifier,
1196
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1197
            'primary_public_key' => $primaryPublicKey,
1198
            'backup_public_key' => $backupPublicKey,
1199
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1200
            'encrypted_secret' => $encryptedSecret,
1201
            'recovery_secret' => $recoverySecret,
1202
            'checksum' => $checksum,
1203
            'key_index' => $keyIndex,
1204
            'segwit' => $segwit,
1205
        ];
1206
1207
        $this->verifyPublicOnly($data);
1208
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1209
        return self::jsonDecode($response->body(), true);
1210
    }
1211
1212
    /**
1213
     * upgrade wallet to use a new account number
1214
     *  the account number specifies which blocktrail cosigning key is used
1215
     *
1216
     * @param string    $identifier             the wallet identifier to be upgraded
1217
     * @param int       $keyIndex               the new account to use
1218
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1219
     * @return mixed
1220
     */
1221
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1222
        $data = [
1223
            'key_index' => $keyIndex,
1224
            'primary_public_key' => $primaryPublicKey
1225
        ];
1226
1227
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1228
        return self::jsonDecode($response->body(), true);
1229
    }
1230
1231
    /**
1232
     * @param array $options
1233
     * @return AddressReaderBase
1234
     */
1235
    private function makeAddressReader(array $options) {
1236
        if ($this->network == "bitcoincash") {
1237
            $useCashAddress = false;
1238
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1239
                $useCashAddress = true;
1240
            }
1241
            return new BitcoinCashAddressReader($useCashAddress);
1242
        } else {
1243
            return new BitcoinAddressReader();
1244
        }
1245
    }
1246
1247
    /**
1248
     * initialize a previously created wallet
1249
     *
1250
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1251
     *
1252
     * Some of the options:
1253
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1254
     *    so the wallet is loaded in read-only mode (no private key)
1255
     *  - "check_backup_key" can be set to your own backup key:
1256
     *    Format: ["M', "xpub..."]
1257
     *    Setting this will allow the SDK to check the server hasn't
1258
     *    a different key (one it happens to control)
1259
1260
     * Either takes one argument:
1261
     * @param array $options
1262
     *
1263
     * Or takes two arguments (old, deprecated syntax):
1264
     * (@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...
1265
     * (@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...
1266
     *
1267
     * @return WalletInterface
1268
     * @throws \Exception
1269
     */
1270
    public function initWallet($options) {
1271
        if (!is_array($options)) {
1272
            $args = func_get_args();
1273
            $options = [
1274
                "identifier" => $args[0],
1275
                "password" => $args[1],
1276
            ];
1277
        }
1278
1279
        $identifier = $options['identifier'];
1280
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1281
                    (isset($options['readOnly']) ? $options['readOnly'] :
1282
                        (isset($options['read-only']) ? $options['read-only'] :
1283
                            false));
1284
1285
        // get the wallet data from the server
1286
        $data = $this->getWallet($identifier);
1287
        if (!$data) {
1288
            throw new \Exception("Failed to get wallet");
1289
        }
1290
1291
        if (array_key_exists('check_backup_key', $options)) {
1292
            if (!is_string($options['check_backup_key'])) {
1293
                throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
1294
            }
1295
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1296
                throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
1297
            }
1298
        }
1299
1300
        $addressReader = $this->makeAddressReader($options);
1301
1302
        switch ($data['wallet_version']) {
1303
            case Wallet::WALLET_VERSION_V1:
1304
                $wallet = new WalletV1(
1305
                    $this,
1306
                    $identifier,
1307
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1308
                    $data['primary_public_keys'],
1309
                    $data['backup_public_key'],
1310
                    $data['blocktrail_public_keys'],
1311
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1312
                    $this->network,
1313
                    $this->testnet,
1314
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1315
                    $addressReader,
1316
                    $data['checksum']
1317
                );
1318
                break;
1319
            case Wallet::WALLET_VERSION_V2:
1320
                $wallet = new WalletV2(
1321
                    $this,
1322
                    $identifier,
1323
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1324
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1325
                    $data['primary_public_keys'],
1326
                    $data['backup_public_key'],
1327
                    $data['blocktrail_public_keys'],
1328
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1329
                    $this->network,
1330
                    $this->testnet,
1331
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1332
                    $addressReader,
1333
                    $data['checksum']
1334
                );
1335
                break;
1336
            case Wallet::WALLET_VERSION_V3:
1337
                if (isset($options['encrypted_primary_seed'])) {
1338
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1339
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1340
                    }
1341
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1342
                } else {
1343
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1344
                }
1345
1346
                if (isset($options['encrypted_secret'])) {
1347
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1348
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1349
                    }
1350
1351
                    $encryptedSecret = $data['encrypted_secret'];
1352
                } else {
1353
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1354
                }
1355
1356
                $wallet = new WalletV3(
1357
                    $this,
1358
                    $identifier,
1359
                    $encryptedPrimarySeed,
1360
                    $encryptedSecret,
1361
                    $data['primary_public_keys'],
1362
                    $data['backup_public_key'],
1363
                    $data['blocktrail_public_keys'],
1364
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1365
                    $this->network,
1366
                    $this->testnet,
1367
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1368
                    $addressReader,
1369
                    $data['checksum']
1370
                );
1371
                break;
1372
            default:
1373
                throw new \InvalidArgumentException("Invalid wallet version");
1374
        }
1375
1376
        if (!$readonly) {
1377
            $wallet->unlock($options);
1378
        }
1379
1380
        return $wallet;
1381
    }
1382
1383
    /**
1384
     * get the wallet data from the server
1385
     *
1386
     * @param string    $identifier             the identifier of the wallet
1387
     * @return mixed
1388
     */
1389
    public function getWallet($identifier) {
1390
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1391
        return self::jsonDecode($response->body(), true);
1392
    }
1393
1394
    /**
1395
     * update the wallet data on the server
1396
     *
1397
     * @param string    $identifier
1398
     * @param $data
1399
     * @return mixed
1400
     */
1401
    public function updateWallet($identifier, $data) {
1402
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1403
        return self::jsonDecode($response->body(), true);
1404
    }
1405
1406
    /**
1407
     * delete a wallet from the server
1408
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1409
     *  is required to be able to delete a wallet
1410
     *
1411
     * @param string    $identifier             the identifier of the wallet
1412
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1413
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1414
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1415
     * @return mixed
1416
     */
1417
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1418
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1419
            'checksum' => $checksumAddress,
1420
            'signature' => $signature
1421
        ], RestClient::AUTH_HTTP_SIG, 360);
1422
        return self::jsonDecode($response->body(), true);
1423
    }
1424
1425
    /**
1426
     * create new backup key;
1427
     *  1) a BIP39 mnemonic
1428
     *  2) a seed from that mnemonic with a blank password
1429
     *  3) a private key from that seed
1430
     *
1431
     * @return array [mnemonic, seed, key]
1432
     */
1433
    protected function newV1BackupSeed() {
1434
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1435
1436
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1437
    }
1438
1439
    /**
1440
     * create new primary key;
1441
     *  1) a BIP39 mnemonic
1442
     *  2) a seed from that mnemonic with the password
1443
     *  3) a private key from that seed
1444
     *
1445
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1446
     * @return array [mnemonic, seed, key]
1447
     * @TODO: require a strong password?
1448
     */
1449
    protected function newV1PrimarySeed($passphrase) {
1450
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1451
1452
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1453
    }
1454
1455
    /**
1456
     * create a new key;
1457
     *  1) a BIP39 mnemonic
1458
     *  2) a seed from that mnemonic with the password
1459
     *  3) a private key from that seed
1460
     *
1461
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1462
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1463
     * @return array
1464
     */
1465
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1466
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1467
        do {
1468
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1469
1470
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1471
1472
            $key = null;
1473
            try {
1474
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1475
            } catch (\Exception $e) {
1476
                // try again
1477
            }
1478
        } while (!$key);
1479
1480
        return [$mnemonic, $seed, $key];
1481
    }
1482
1483
    /**
1484
     * generate a new mnemonic from some random entropy (512 bit)
1485
     *
1486
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1487
     * @return string
1488
     * @throws \Exception
1489
     */
1490
    public function generateNewMnemonic($forceEntropy = null) {
1491
        if ($forceEntropy === null) {
1492
            $random = new Random();
1493
            $entropy = $random->bytes(512 / 8);
1494
        } else {
1495
            $entropy = $forceEntropy;
1496
        }
1497
1498
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1495 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...
1499
    }
1500
1501
    /**
1502
     * get the balance for the wallet
1503
     *
1504
     * @param string    $identifier             the identifier of the wallet
1505
     * @return array
1506
     */
1507
    public function getWalletBalance($identifier) {
1508
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1509
        return self::jsonDecode($response->body(), true);
1510
    }
1511
1512
    /**
1513
     * get a new derivation number for specified parent path
1514
     *  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
1515
     *
1516
     * returns the path
1517
     *
1518
     * @param string    $identifier             the identifier of the wallet
1519
     * @param string    $path                   the parent path for which to get a new derivation
1520
     * @return string
1521
     */
1522
    public function getNewDerivation($identifier, $path) {
1523
        $result = $this->_getNewDerivation($identifier, $path);
1524
        return $result['path'];
1525
    }
1526
1527
    /**
1528
     * get a new derivation number for specified parent path
1529
     *  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
1530
     *
1531
     * @param string    $identifier             the identifier of the wallet
1532
     * @param string    $path                   the parent path for which to get a new derivation
1533
     * @return mixed
1534
     */
1535
    public function _getNewDerivation($identifier, $path) {
1536
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1537
        return self::jsonDecode($response->body(), true);
1538
    }
1539
1540
    /**
1541
     * get the path (and redeemScript) to specified address
1542
     *
1543
     * @param string $identifier
1544
     * @param string $address
1545
     * @return array
1546
     * @throws \Exception
1547
     */
1548
    public function getPathForAddress($identifier, $address) {
1549
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1550
        return self::jsonDecode($response->body(), true)['path'];
1551
    }
1552
1553
    /**
1554
     * send the transaction using the API
1555
     *
1556
     * @param string       $identifier     the identifier of the wallet
1557
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1558
     * @param array        $paths          list of the paths that were used for the UTXO
1559
     * @param bool         $checkFee       let the server verify the fee after signing
1560
     * @param null         $twoFactorToken
1561
     * @return string                                the complete raw transaction
1562
     * @throws \Exception
1563
     */
1564
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1565
        $data = [
1566
            'paths' => $paths,
1567
            'two_factor_token' => $twoFactorToken,
1568
        ];
1569
1570
        if (is_array($rawTransaction)) {
1571
            if (array_key_exists('base_transaction', $rawTransaction)
1572
            && array_key_exists('signed_transaction', $rawTransaction)) {
1573
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1574
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1575
            } else {
1576
                throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1577
            }
1578
        } else {
1579
            $data['raw_transaction'] = $rawTransaction;
1580
        }
1581
1582
        // dynamic TTL for when we're signing really big transactions
1583
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1584
1585
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1586
        $signed = self::jsonDecode($response->body(), true);
1587
1588
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1589
            throw new \Exception("Failed to completely sign transaction");
1590
        }
1591
1592
        // create TX hash from the raw signed hex
1593
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1594
    }
1595
1596
    /**
1597
     * use the API to get the best inputs to use based on the outputs
1598
     *
1599
     * the return array has the following format:
1600
     * [
1601
     *  "utxos" => [
1602
     *      [
1603
     *          "hash" => "<txHash>",
1604
     *          "idx" => "<index of the output of that <txHash>",
1605
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1606
     *          "value" => 32746327,
1607
     *          "address" => "1address",
1608
     *          "path" => "m/44'/1'/0'/0/13",
1609
     *          "redeem_script" => "<redeemScript-hex>",
1610
     *      ],
1611
     *  ],
1612
     *  "fee"   => 10000,
1613
     *  "change"=> 1010109201,
1614
     * ]
1615
     *
1616
     * @param string   $identifier              the identifier of the wallet
1617
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1618
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1619
     *                                          so you have some time to spend them without race-conditions
1620
     * @param bool     $allowZeroConf
1621
     * @param string   $feeStrategy
1622
     * @param null|int $forceFee
1623
     * @return array
1624
     * @throws \Exception
1625
     */
1626
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1627
        $args = [
1628
            'lock' => (int)!!$lockUTXO,
1629
            'zeroconf' => (int)!!$allowZeroConf,
1630
            'fee_strategy' => $feeStrategy,
1631
        ];
1632
1633
        if ($forceFee !== null) {
1634
            $args['forcefee'] = (int)$forceFee;
1635
        }
1636
1637
        $response = $this->blocktrailClient->post(
1638
            "wallet/{$identifier}/coin-selection",
1639
            $args,
1640
            $outputs,
1641
            RestClient::AUTH_HTTP_SIG
1642
        );
1643
1644
        \var_export(self::jsonDecode($response->body(), true));
1645
1646
        return self::jsonDecode($response->body(), true);
1647
    }
1648
1649
    /**
1650
     *
1651
     * @param string   $identifier the identifier of the wallet
1652
     * @param bool     $allowZeroConf
1653
     * @param string   $feeStrategy
1654
     * @param null|int $forceFee
1655
     * @param int      $outputCnt
1656
     * @return array
1657
     * @throws \Exception
1658
     */
1659
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1660
        $args = [
1661
            'zeroconf' => (int)!!$allowZeroConf,
1662
            'fee_strategy' => $feeStrategy,
1663
            'outputs' => $outputCnt,
1664
        ];
1665
1666
        if ($forceFee !== null) {
1667
            $args['forcefee'] = (int)$forceFee;
1668
        }
1669
1670
        $response = $this->blocktrailClient->get(
1671
            "wallet/{$identifier}/max-spendable",
1672
            $args,
1673
            RestClient::AUTH_HTTP_SIG
1674
        );
1675
1676
        return self::jsonDecode($response->body(), true);
1677
    }
1678
1679
    /**
1680
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1681
     */
1682
    public function feePerKB() {
1683
        $response = $this->blocktrailClient->get("fee-per-kb");
1684
        return self::jsonDecode($response->body(), true);
1685
    }
1686
1687
    /**
1688
     * get the current price index
1689
     *
1690
     * @return array        eg; ['USD' => 287.30]
1691
     */
1692 2
    public function price() {
1693 2
        $response = $this->blocktrailClient->get("price");
1694 1
        return self::jsonDecode($response->body(), true);
1695
    }
1696
1697
    /**
1698
     * setup webhook for wallet
1699
     *
1700
     * @param string    $identifier         the wallet identifier for which to create the webhook
1701
     * @param string    $webhookIdentifier  the webhook identifier to use
1702
     * @param string    $url                the url to receive the webhook events
1703
     * @return array
1704
     */
1705
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1706
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1707
        return self::jsonDecode($response->body(), true);
1708
    }
1709
1710
    /**
1711
     * delete webhook for wallet
1712
     *
1713
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1714
     * @param string    $webhookIdentifier  the webhook identifier to delete
1715
     * @return array
1716
     */
1717
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1718
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1719
        return self::jsonDecode($response->body(), true);
1720
    }
1721
1722
    /**
1723
     * lock a specific unspent output
1724
     *
1725
     * @param     $identifier
1726
     * @param     $txHash
1727
     * @param     $txIdx
1728
     * @param int $ttl
1729
     * @return bool
1730
     */
1731
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1732
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1733
        return self::jsonDecode($response->body(), true)['locked'];
1734
    }
1735
1736
    /**
1737
     * unlock a specific unspent output
1738
     *
1739
     * @param     $identifier
1740
     * @param     $txHash
1741
     * @param     $txIdx
1742
     * @return bool
1743
     */
1744
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1745
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1746
        return self::jsonDecode($response->body(), true)['unlocked'];
1747
    }
1748
1749
    /**
1750
     * get all transactions for wallet (paginated)
1751
     *
1752
     * @param  string  $identifier  the wallet identifier for which to get transactions
1753
     * @param  integer $page        pagination: page number
1754
     * @param  integer $limit       pagination: records per page (max 500)
1755
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1756
     * @return array                associative array containing the response
1757
     */
1758
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1759
        $queryString = [
1760
            'page' => $page,
1761
            'limit' => $limit,
1762
            'sort_dir' => $sortDir
1763
        ];
1764
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1765
        return self::jsonDecode($response->body(), true);
1766
    }
1767
1768
    /**
1769
     * get all addresses for wallet (paginated)
1770
     *
1771
     * @param  string  $identifier  the wallet identifier for which to get addresses
1772
     * @param  integer $page        pagination: page number
1773
     * @param  integer $limit       pagination: records per page (max 500)
1774
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1775
     * @return array                associative array containing the response
1776
     */
1777
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1778
        $queryString = [
1779
            'page' => $page,
1780
            'limit' => $limit,
1781
            'sort_dir' => $sortDir
1782
        ];
1783
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1784
        return self::jsonDecode($response->body(), true);
1785
    }
1786
1787
    /**
1788
     * get all UTXOs for wallet (paginated)
1789
     *
1790
     * @param  string  $identifier  the wallet identifier for which to get addresses
1791
     * @param  integer $page        pagination: page number
1792
     * @param  integer $limit       pagination: records per page (max 500)
1793
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1794
     * @param  boolean $zeroconf    include zero confirmation transactions
1795
     * @return array                associative array containing the response
1796
     */
1797
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1798
        $queryString = [
1799
            'page' => $page,
1800
            'limit' => $limit,
1801
            'sort_dir' => $sortDir,
1802
            'zeroconf' => (int)!!$zeroconf,
1803
        ];
1804
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1805
        return self::jsonDecode($response->body(), true);
1806
    }
1807
1808
    /**
1809
     * get a paginated list of all wallets associated with the api user
1810
     *
1811
     * @param  integer          $page    pagination: page number
1812
     * @param  integer          $limit   pagination: records per page
1813
     * @return array                     associative array containing the response
1814
     */
1815
    public function allWallets($page = 1, $limit = 20) {
1816
        $queryString = [
1817
            'page' => $page,
1818
            'limit' => $limit
1819
        ];
1820
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1821
        return self::jsonDecode($response->body(), true);
1822
    }
1823
1824
    /**
1825
     * send raw transaction
1826
     *
1827
     * @param     $txHex
1828
     * @return bool
1829
     */
1830
    public function sendRawTransaction($txHex) {
1831
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1832
        return self::jsonDecode($response->body(), true);
1833
    }
1834
1835
    /**
1836
     * testnet only ;-)
1837
     *
1838
     * @param     $address
1839
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1840
     * @return mixed
1841
     * @throws \Exception
1842
     */
1843
    public function faucetWithdrawal($address, $amount = 10000) {
1844
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1845
            'address' => $address,
1846
            'amount' => $amount,
1847
        ], RestClient::AUTH_HTTP_SIG);
1848
        return self::jsonDecode($response->body(), true);
1849
    }
1850
1851
    /**
1852
     * Exists for BC. Remove at major bump.
1853
     *
1854
     * @see faucetWithdrawal
1855
     * @deprecated
1856
     * @param     $address
1857
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1858
     * @return mixed
1859
     * @throws \Exception
1860
     */
1861
    public function faucetWithdrawl($address, $amount = 10000) {
1862
        return $this->faucetWithdrawal($address, $amount);
1863
    }
1864
1865
    /**
1866
     * verify a message signed bitcoin-core style
1867
     *
1868
     * @param  string           $message
1869
     * @param  string           $address
1870
     * @param  string           $signature
1871
     * @return boolean
1872
     */
1873 2
    public function verifyMessage($message, $address, $signature) {
1874 2
        $adapter = Bitcoin::getEcAdapter();
1875 2
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1876 2
        if (!$addr instanceof PayToPubKeyHashAddress) {
1877
            throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
1878
        }
1879
1880
        /** @var CompactSignatureSerializerInterface $csSerializer */
1881 2
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Documentation introduced by
$adapter is of type object<BitWasp\Bitcoin\C...ter\EcAdapterInterface>, but the function expects a boolean|object<BitWasp\B...\Crypto\EcAdapter\true>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1882 2
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1883
1884 2
        $signer = new MessageSigner($adapter);
1885 2
        return $signer->verify($signedMessage, $addr);
1886
    }
1887
1888
    /**
1889
     * Take a base58 or cashaddress, and return only
1890
     * the cash address.
1891
     * This function only works on bitcoin cash.
1892
     * @param string $input
1893
     * @return string
1894
     * @throws BlocktrailSDKException
1895
     */
1896
    public function getLegacyBitcoinCashAddress($input) {
1897
        if ($this->network === "bitcoincash") {
1898
            $address = $this
1899
                ->makeAddressReader([
1900
                    "use_cashaddress" => true
1901
                ])
1902
                ->fromString($input);
1903
1904
            if ($address instanceof CashAddress) {
1905
                $address = $address->getLegacyAddress();
1906
            }
1907
1908
            return $address->getAddress();
1909
        }
1910
1911
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1912
    }
1913
1914
    /**
1915
     * convert a Satoshi value to a BTC value
1916
     *
1917
     * @param int       $satoshi
1918
     * @return float
1919
     */
1920 1
    public static function toBTC($satoshi) {
1921 1
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1922
    }
1923
1924
    /**
1925
     * convert a Satoshi value to a BTC value and return it as a string
1926
1927
     * @param int       $satoshi
1928
     * @return string
1929
     */
1930
    public static function toBTCString($satoshi) {
1931
        return sprintf("%.8f", self::toBTC($satoshi));
1932
    }
1933
1934
    /**
1935
     * convert a BTC value to a Satoshi value
1936
     *
1937
     * @param float     $btc
1938
     * @return string
1939
     */
1940 1
    public static function toSatoshiString($btc) {
1941 1
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1942
    }
1943
1944
    /**
1945
     * convert a BTC value to a Satoshi value
1946
     *
1947
     * @param float     $btc
1948
     * @return string
1949
     */
1950 1
    public static function toSatoshi($btc) {
1951 1
        return (int)self::toSatoshiString($btc);
1952
    }
1953
1954
    /**
1955
     * json_decode helper that throws exceptions when it fails to decode
1956
     *
1957
     * @param      $json
1958
     * @param bool $assoc
1959
     * @return mixed
1960
     * @throws \Exception
1961
     */
1962 4
    public static function jsonDecode($json, $assoc = false) {
1963 4
        if (!$json) {
1964
            throw new \Exception("Can't json_decode empty string [{$json}]");
1965
        }
1966
1967 4
        $data = json_decode($json, $assoc);
1968
1969 4
        if ($data === null) {
1970
            throw new \Exception("Failed to json_decode [{$json}]");
1971
        }
1972
1973 4
        return $data;
1974
    }
1975
1976
    /**
1977
     * sort public keys for multisig script
1978
     *
1979
     * @param PublicKeyInterface[] $pubKeys
1980
     * @return PublicKeyInterface[]
1981
     */
1982
    public static function sortMultisigKeys(array $pubKeys) {
1983
        $result = array_values($pubKeys);
1984
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1985
            $av = $a->getHex();
1986
            $bv = $b->getHex();
1987
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1988
        });
1989
1990
        return $result;
1991
    }
1992
1993
    /**
1994
     * read and decode the json payload from a webhook's POST request.
1995
     *
1996
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1997
     * @return mixed|null
1998
     * @throws \Exception
1999
     */
2000
    public static function getWebhookPayload($returnObject = false) {
2001
        $data = file_get_contents("php://input");
2002
        if ($data) {
2003
            return self::jsonDecode($data, !$returnObject);
2004
        } else {
2005
            return null;
2006
        }
2007
    }
2008
2009
    public static function normalizeBIP32KeyArray($keys) {
2010
        return Util::arrayMapWithIndex(function ($idx, $key) {
2011
            return [$idx, self::normalizeBIP32Key($key)];
2012
        }, $keys);
2013
    }
2014
2015
    /**
2016
     * @param array|BIP32Key $key
2017
     * @return BIP32Key
2018
     * @throws \Exception
2019
     */
2020
    public static function normalizeBIP32Key($key) {
2021
        if ($key instanceof BIP32Key) {
2022
            return $key;
2023
        }
2024
2025
        if (is_array($key) && count($key) === 2) {
2026
            $path = $key[1];
2027
            $hk = $key[0];
2028
2029
            if (!($hk instanceof HierarchicalKey)) {
2030
                $hk = HierarchicalKeyFactory::fromExtended($hk);
2031
            }
2032
2033
            return BIP32Key::create($hk, $path);
2034
        } else {
2035
            throw new \Exception("Bad Input");
2036
        }
2037
    }
2038
2039
    public function shuffle($arr) {
2040
        \shuffle($arr);
2041
    }
2042
}
2043