Passed
Pull Request — master (#129)
by thomas
26:40 queued 15:56
created

BlocktrailSDK::sendTransaction()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 30
c 0
b 0
f 0
ccs 0
cts 12
cp 0
rs 9.0444
cc 6
nc 5
nop 5
crap 42
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use Btccom\BitcoinCash\Address\AddressCreator as BitcoinCashAddressCreator;
6
use Btccom\BitcoinCash\Address\CashAddress;
7
use Btccom\BitcoinCash\Network\NetworkFactory as BitcoinCashNetworkFactory;
8
use BitWasp\Bitcoin\Address\AddressCreator as BitcoinAddressCreator;
9
use BitWasp\Bitcoin\Address\BaseAddressCreator;
10
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
11
use BitWasp\Bitcoin\Bitcoin;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
13
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
14
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
15
use BitWasp\Bitcoin\Crypto\Random\Random;
16
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
17
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
18
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
19
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
20
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
21
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
22
use BitWasp\Bitcoin\Network\NetworkFactory as BitcoinNetworkFactory;
23
use BitWasp\Bitcoin\Transaction\TransactionFactory;
24
use BitWasp\Buffertools\Buffer;
25
use BitWasp\Buffertools\BufferInterface;
26
use Blocktrail\CryptoJSAES\CryptoJSAES;
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\Connection\RestClientInterface;
33
use Btccom\JustEncrypt\Encryption;
34
use Btccom\JustEncrypt\EncryptionMnemonic;
35
use Btccom\JustEncrypt\KeyDerivation;
36
37
/**
38
 * Class BlocktrailSDK
39
 */
40
class BlocktrailSDK implements BlocktrailSDKInterface {
41
    /**
42
     * @var Connection\RestClientInterface
43
     */
44
    protected $blocktrailClient;
45
46
    /**
47
     * @var Connection\RestClient
48
     */
49
    protected $dataClient;
50
51
    /**
52
     * @var string          currently only supporting; bitcoin
53
     */
54
    protected $network;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $testnet;
60
61
    /**
62
     * @var ConverterInterface
63
     */
64
    protected $converter;
65
66
    /**
67
     * @param   string      $apiKey         the API_KEY to use for authentication
68
     * @param   string      $apiSecret      the API_SECRET to use for authentication
69
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
70
     * @param   bool        $testnet        testnet yes/no
71
     * @param   string      $apiVersion     the version of the API to consume
72
     * @param   null        $apiEndpoint    overwrite the endpoint used
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $apiEndpoint is correct as it would always require null to be passed?
Loading history...
73
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
74
     */
75
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
76
77
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
78 119
79
        if (is_null($apiEndpoint)) {
0 ignored issues
show
introduced by
The condition is_null($apiEndpoint) is always true.
Loading history...
80 119
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://wallet-api.btc.com";
81
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
82 119
        }
83 119
84 119
        // normalize network and set bitcoinlib to the right magic-bytes
85
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
86
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
87
88 119
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT');
89 119
        if (!$btccomEndpoint) {
90
            $btccomEndpoint = "https://" . ($this->network === "bitcoincash" ? "bch-chain" : "chain") . ".api.btc.com";
91 119
        }
92 119
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
93
94
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
95 119
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
96
        }
97 119
98 33
        if ($throttle = \getenv('BLOCKTRAIL_SDK_THROTTLE_BTCCOM')) {
99
            $throttle = (float)$throttle;
100
        } else {
101 119
            $throttle = 0.33;
102 119
        }
103
104 119
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
105 119
        $this->dataClient->setThrottle($throttle);
106
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
107
        $this->converter = new BtccomConverter($this->makeAddressReader([
108
            'use_cashaddr' => true,
109
        ]));
110
    }
111
112
    /**
113
     * normalize network string
114
     *
115 119
     * @param $network
116
     * @param $testnet
117 119
     * @return array
118
     * @throws \Exception
119
     */
120
    protected function normalizeNetwork($network, $testnet) {
121
        // [name, testnet, network]
122
        return Util::normalizeNetwork($network, $testnet);
123
    }
124
125
    /**
126
     * set BitcoinLib to the correct magic-byte defaults for the selected network
127 119
     *
128
     * @param $network
129 119
     * @param bool $testnet
130 119
     * @param bool $regtest
131
     */
132 119
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
133 29
134
        if ($network === "bitcoin") {
135 119
            if ($regtest) {
136
                $useNetwork = BitcoinNetworkFactory::bitcoinRegtest();
137 4
            } else if ($testnet) {
138 4
                $useNetwork = BitcoinNetworkFactory::bitcoinTestnet();
139
            } else {
140 4
                $useNetwork = BitcoinNetworkFactory::bitcoin();
141 4
            }
142
        } else if ($network === "bitcoincash") {
143
            if ($regtest) {
144
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCashRegtest();
145
            } else if ($testnet) {
146
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCashTestnet();
147 119
            } else {
148 119
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCash();
149
            }
150
        }
151
152
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.
Loading history...
153
    }
154
155
    /**
156
     * enable CURL debugging output
157
     *
158
     * @param   bool        $debug
159
     *
160
     * @codeCoverageIgnore
161
     */
162
    public function setCurlDebugging($debug = true) {
163
        $this->blocktrailClient->setCurlDebugging($debug);
0 ignored issues
show
Bug introduced by
The method setCurlDebugging() does not exist on Blocktrail\SDK\Connection\RestClientInterface. It seems like you code against a sub-type of Blocktrail\SDK\Connection\RestClientInterface such as Blocktrail\SDK\Connection\RestClient. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
        $this->blocktrailClient->/** @scrutinizer ignore-call */ 
164
                                 setCurlDebugging($debug);
Loading history...
164
        $this->dataClient->setCurlDebugging($debug);
165
    }
166
167
    /**
168
     * enable verbose errors
169
     *
170
     * @param   bool        $verboseErrors
171
     *
172
     * @codeCoverageIgnore
173
     */
174
    public function setVerboseErrors($verboseErrors = true) {
175
        $this->blocktrailClient->setVerboseErrors($verboseErrors);
176
        $this->dataClient->setVerboseErrors($verboseErrors);
177
    }
178
    
179
    /**
180
     * set cURL default option on Guzzle client
181
     * @param string    $key
182
     * @param bool      $value
183
     *
184
     * @codeCoverageIgnore
185
     */
186
    public function setCurlDefaultOption($key, $value) {
187
        $this->blocktrailClient->setCurlDefaultOption($key, $value);
0 ignored issues
show
Bug introduced by
The method setCurlDefaultOption() does not exist on Blocktrail\SDK\Connection\RestClientInterface. It seems like you code against a sub-type of Blocktrail\SDK\Connection\RestClientInterface such as Blocktrail\SDK\Connection\RestClient. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

187
        $this->blocktrailClient->/** @scrutinizer ignore-call */ 
188
                                 setCurlDefaultOption($key, $value);
Loading history...
188
        $this->dataClient->setCurlDefaultOption($key, $value);
189 2
    }
190 2
191
    /**
192
     * @return  RestClientInterface
193
     */
194
    public function getRestClient() {
195
        return $this->blocktrailClient;
196
    }
197
198
    /**
199
     * @return  RestClient
200
     */
201
    public function getDataRestClient() {
202
        return $this->dataClient;
203
    }
204
205
    /**
206
     * @param RestClientInterface $restClient
207
     */
208
    public function setRestClient(RestClientInterface $restClient) {
209
        $this->blocktrailClient = $restClient;
210
    }
211
212 1
    /**
213 1
     * get a single address
214 1
     * @param  string $address address hash
215
     * @return array           associative array containing the response
216
     */
217
    public function address($address) {
218
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
219
        return $this->converter->convertAddress($response->body());
220
    }
221
222
    /**
223
     * get all transactions for an address (paginated)
224
     * @param  string  $address address hash
225 1
     * @param  integer $page    pagination: page number
226
     * @param  integer $limit   pagination: records per page (max 500)
227 1
     * @param  string  $sortDir pagination: sort direction (asc|desc)
228 1
     * @return array            associative array containing the response
229 1
     */
230
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
231 1
        $queryString = [
232 1
            'page' => $page,
233
            'limit' => $limit,
234
            'sort_dir' => $sortDir,
235
        ];
236
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
237
        return $this->converter->convertAddressTxs($response->body());
238
    }
239
240
    /**
241
     * get all unconfirmed transactions for an address (paginated)
242
     * @param  string  $address address hash
243
     * @param  integer $page    pagination: page number
244
     * @param  integer $limit   pagination: records per page (max 500)
245
     * @param  string  $sortDir pagination: sort direction (asc|desc)
246
     * @return array            associative array containing the response
247
     */
248
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
249
        $queryString = [
250
            'page' => $page,
251
            'limit' => $limit,
252
            'sort_dir' => $sortDir
253
        ];
254
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
255
        return $this->converter->convertAddressTxs($response->body());
256
    }
257
258
    /**
259
     * get all unspent outputs for an address (paginated)
260
     * @param  string  $address address hash
261 1
     * @param  integer $page    pagination: page number
262
     * @param  integer $limit   pagination: records per page (max 500)
263 1
     * @param  string  $sortDir pagination: sort direction (asc|desc)
264 1
     * @return array            associative array containing the response
265 1
     */
266
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
267 1
        $queryString = [
268 1
            'page' => $page,
269
            'limit' => $limit,
270
            'sort_dir' => $sortDir
271
        ];
272
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
273
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
274
    }
275
276
    /**
277
     * get all unspent outputs for a batch of addresses (paginated)
278
     *
279
     * @param  string[] $addresses
280
     * @param  integer  $page    pagination: page number
281
     * @param  integer  $limit   pagination: records per page (max 500)
282
     * @param  string   $sortDir pagination: sort direction (asc|desc)
283
     * @return array associative array containing the response
284
     * @throws \Exception
285
     */
286
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
287
        $queryString = [
288
            'page' => $page,
289
            'limit' => $limit,
290
            'sort_dir' => $sortDir
291
        ];
292
293
        if ($this->converter instanceof BtccomConverter) {
294
            if ($page > 1) {
295
                return [
296
                    'data' => [],
297
                    'current_page' => 2,
298
                    'per_page' => null,
299
                    'total' => null,
300
                ];
301
            }
302
303
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
304
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
305
        } else {
306
            $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
0 ignored issues
show
Bug Best Practice introduced by
The property client does not exist on Blocktrail\SDK\BlocktrailSDK. Did you maybe forget to declare it?
Loading history...
307
            return self::jsonDecode($response->body(), true);
308
        }
309
    }
310
311
    /**
312 1
     * verify ownership of an address
313 1
     * @param  string  $address     address hash
314 1
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
315
     * @return array                associative array containing the response
316
     */
317
    public function verifyAddress($address, $signature) {
318
        if ($this->verifyMessage($address, $address, $signature)) {
319
            return ['result' => true, 'msg' => 'Successfully verified'];
320
        } else {
321
            return ['result' => false];
322
        }
323
    }
324
325
    /**
326
     * get all blocks (paginated)
327 1
     * @param  integer $page    pagination: page number
328
     * @param  integer $limit   pagination: records per page
329 1
     * @param  string  $sortDir pagination: sort direction (asc|desc)
330 1
     * @return array            associative array containing the response
331 1
     */
332
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
333 1
        $queryString = [
334 1
            'page' => $page,
335
            'limit' => $limit,
336
            'sort_dir' => $sortDir
337
        ];
338
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
339
        return $this->converter->convertBlocks($response->body());
340
    }
341 1
342 1
    /**
343 1
     * get the latest block
344
     * @return array            associative array containing the response
345
     */
346
    public function blockLatest() {
347
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
348
        return $this->converter->convertBlock($response->body());
349
    }
350 1
351 1
    /**
352
     * get the wallet API's latest block ['hash' => x, 'height' => y]
353
     * @return array            associative array containing the response
354
     */
355
    public function getWalletBlockLatest() {
356
        $response = $this->blocktrailClient->get("block/latest");
357
        return BlocktrailSDK::jsonDecode($response->body(), true);
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...
358
    }
359
360 1
    /**
361 1
     * get an individual block
362 1
     * @param  string|integer $block    a block hash or a block height
363
     * @return array                    associative array containing the response
364
     */
365
    public function block($block) {
366
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
367
        return $this->converter->convertBlock($response->body());
368
    }
369
370
    /**
371
     * get all transaction in a block (paginated)
372
     * @param  string|integer   $block   a block hash or a block height
373
     * @param  integer          $page    pagination: page number
374
     * @param  integer          $limit   pagination: records per page
375
     * @param  string           $sortDir pagination: sort direction (asc|desc)
376
     * @return array                     associative array containing the response
377
     */
378
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
379
        $queryString = [
380
            'page' => $page,
381
            'limit' => $limit,
382
            'sort_dir' => $sortDir
383
        ];
384
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
385
        return $this->converter->convertBlockTxs($response->body());
386
    }
387
388 1
    /**
389 1
     * get a single transaction
390 1
     * @param  string $txhash transaction hash
391
     * @return array          associative array containing the response
392 1
     */
393 1
    public function transaction($txhash) {
394
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
395
        $res = $this->converter->convertTx($response->body(), null);
396 1
397
        if ($this->converter instanceof BtccomConverter) {
398
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
399
        }
400
401
        return $res;
402
    }
403
404 1
    /**
405 1
     * get a single transaction
406 1
     * @param  string[] $txhashes list of transaction hashes (up to 20)
407
     * @return array[]            array containing the response
408
     */
409
    public function transactions($txhashes) {
410
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
411
        return $this->converter->convertTxs($response->body());
412
    }
413
    
414
    /**
415
     * get a paginated list of all webhooks associated with the api user
416
     * @param  integer          $page    pagination: page number
417
     * @param  integer          $limit   pagination: records per page
418
     * @return array                     associative array containing the response
419
     */
420
    public function allWebhooks($page = 1, $limit = 20) {
421
        $queryString = [
422
            'page' => $page,
423
            'limit' => $limit
424
        ];
425
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
426
        return self::jsonDecode($response->body(), true);
427
    }
428
429
    /**
430
     * get an existing webhook by it's identifier
431
     * @param string    $identifier     a unique identifier associated with the webhook
432
     * @return array                    associative array containing the response
433
     */
434
    public function getWebhook($identifier) {
435
        $response = $this->blocktrailClient->get("webhook/".$identifier);
436
        return self::jsonDecode($response->body(), true);
437
    }
438
439
    /**
440 1
     * create a new webhook
441
     * @param  string  $url        the url to receive the webhook events
442 1
     * @param  string  $identifier a unique identifier to associate with this webhook
443 1
     * @return array               associative array containing the response
444
     */
445 1
    public function setupWebhook($url, $identifier = null) {
446
        $postData = [
447
            'url'        => $url,
448
            'identifier' => $identifier
449
        ];
450
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
451
        return self::jsonDecode($response->body(), true);
452
    }
453
454
    /**
455
     * update an existing webhook
456
     * @param  string  $identifier      the unique identifier of the webhook to update
457
     * @param  string  $newUrl          the new url to receive the webhook events
458
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
459
     * @return array                    associative array containing the response
460
     */
461
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
462
        $putData = [
463
            'url'        => $newUrl,
464
            'identifier' => $newIdentifier
465
        ];
466
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
467
        return self::jsonDecode($response->body(), true);
468
    }
469
470
    /**
471
     * deletes an existing webhook and any event subscriptions associated with it
472
     * @param  string  $identifier      the unique identifier of the webhook to delete
473
     * @return boolean                  true on success
474
     */
475
    public function deleteWebhook($identifier) {
476
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
477
        return self::jsonDecode($response->body(), true);
478
    }
479
480
    /**
481
     * get a paginated list of all the events a webhook is subscribed to
482
     * @param  string  $identifier  the unique identifier of the webhook
483
     * @param  integer $page        pagination: page number
484
     * @param  integer $limit       pagination: records per page
485
     * @return array                associative array containing the response
486
     */
487
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
488
        $queryString = [
489
            'page' => $page,
490
            'limit' => $limit
491
        ];
492
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
493
        return self::jsonDecode($response->body(), true);
494
    }
495
    
496
    /**
497
     * subscribes a webhook to transaction events of one particular transaction
498
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
499
     * @param  string  $transaction     the transaction hash
500
     * @param  integer $confirmations   the amount of confirmations to send.
501
     * @return array                    associative array containing the response
502
     */
503
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
504
        $postData = [
505
            'event_type'    => 'transaction',
506
            'transaction'   => $transaction,
507
            'confirmations' => $confirmations,
508
        ];
509
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
510
        return self::jsonDecode($response->body(), true);
511
    }
512
513
    /**
514
     * subscribes a webhook to transaction events on a particular address
515
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
516
     * @param  string  $address         the address hash
517
     * @param  integer $confirmations   the amount of confirmations to send.
518
     * @return array                    associative array containing the response
519
     */
520
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
521
        $postData = [
522
            'event_type'    => 'address-transactions',
523
            'address'       => $address,
524
            'confirmations' => $confirmations,
525
        ];
526
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
527
        return self::jsonDecode($response->body(), true);
528
    }
529
530
    /**
531
     * batch subscribes a webhook to multiple transaction events
532
     *
533
     * @param  string $identifier   the unique identifier of the webhook
534
     * @param  array  $batchData    A 2D array of event data:
535
     *                              [address => $address, confirmations => $confirmations]
536
     *                              where $address is the address to subscibe to
537
     *                              and optionally $confirmations is the amount of confirmations
538
     * @return boolean              true on success
539
     */
540
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
541
        $postData = [];
542
        foreach ($batchData as $record) {
543
            $postData[] = [
544
                'event_type' => 'address-transactions',
545
                'address' => $record['address'],
546
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
547
            ];
548
        }
549
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
550
        return self::jsonDecode($response->body(), true);
551
    }
552
553
    /**
554
     * subscribes a webhook to a new block event
555
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
556
     * @return array                associative array containing the response
557
     */
558
    public function subscribeNewBlocks($identifier) {
559
        $postData = [
560
            'event_type'    => 'block',
561
        ];
562
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
563
        return self::jsonDecode($response->body(), true);
564
    }
565
566
    /**
567
     * removes an transaction event subscription from a webhook
568
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
569
     * @param  string  $transaction     the transaction hash of the event subscription
570
     * @return boolean                  true on success
571
     */
572
    public function unsubscribeTransaction($identifier, $transaction) {
573
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
574
        return self::jsonDecode($response->body(), true);
575
    }
576
577
    /**
578
     * removes an address transaction event subscription from a webhook
579
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
580
     * @param  string  $address         the address hash of the event subscription
581
     * @return boolean                  true on success
582
     */
583
    public function unsubscribeAddressTransactions($identifier, $address) {
584
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
585
        return self::jsonDecode($response->body(), true);
586
    }
587
588
    /**
589
     * removes a block event subscription from a webhook
590
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
591
     * @return boolean                  true on success
592
     */
593
    public function unsubscribeNewBlocks($identifier) {
594
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
595
        return self::jsonDecode($response->body(), true);
596
    }
597
598
    /**
599
     * create a new wallet
600
     *   - will generate a new primary seed (with password) and backup seed (without password)
601
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
602
     *   - receive the blocktrail co-signing public key from the server
603
     *
604
     * Either takes one argument:
605
     * @param array $options
606
     *
607
     * Or takes three arguments (old, deprecated syntax):
608
     * (@nonPHP-doc) @param      $identifier
609
     * (@nonPHP-doc) @param      $password
610
     * (@nonPHP-doc) @param int  $keyIndex          override for the blocktrail cosigning key to use
611
     *
612
     * @return array[WalletInterface, array]      list($wallet, $backupInfo)
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[WalletInterface, array] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
613
     * @throws \Exception
614
     */
615
    public function createNewWallet($options) {
616
        if (!is_array($options)) {
617
            $args = func_get_args();
618
            $options = [
619
                "identifier" => $args[0],
620
                "password" => $args[1],
621
                "key_index" => isset($args[2]) ? $args[2] : null,
622
            ];
623
        }
624
625
        if (isset($options['password'])) {
626
            if (isset($options['passphrase'])) {
627
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
628
            } else {
629
                $options['passphrase'] = $options['password'];
630
            }
631
        }
632
633
        if (!isset($options['passphrase'])) {
634
            $options['passphrase'] = null;
635
        }
636
637
        if (!isset($options['key_index'])) {
638
            $options['key_index'] = 0;
639
        }
640
641
        if (!isset($options['wallet_version'])) {
642
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
643
        }
644
645
        switch ($options['wallet_version']) {
646
            case Wallet::WALLET_VERSION_V1:
647
                return $this->createNewWalletV1($options);
648
649
            case Wallet::WALLET_VERSION_V2:
650
                return $this->createNewWalletV2($options);
651
652
            case Wallet::WALLET_VERSION_V3:
653
                return $this->createNewWalletV3($options);
654
655
            default:
656
                throw new \InvalidArgumentException("Invalid wallet version");
657
        }
658
    }
659
660
    protected function createNewWalletV1($options) {
661
        $walletPath = WalletPath::create($options['key_index']);
662
663
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
664
665
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
666
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
667
        }
668
669
        $primaryMnemonic = null;
670
        $primaryPrivateKey = null;
671
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
672
            if (!$options['passphrase']) {
673
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
674
            } else {
675
                // create new primary seed
676
                /** @var HierarchicalKey $primaryPrivateKey */
677
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
678
                if ($storePrimaryMnemonic !== false) {
679
                    $storePrimaryMnemonic = true;
680
                }
681
            }
682
        } elseif (isset($options['primary_mnemonic'])) {
683
            $primaryMnemonic = $options['primary_mnemonic'];
684
        } elseif (isset($options['primary_private_key'])) {
685
            $primaryPrivateKey = $options['primary_private_key'];
686
        }
687
688
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
689
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
690
        }
691
692
        if ($primaryPrivateKey) {
693
            if (is_string($primaryPrivateKey)) {
694
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
695
            }
696
        } else {
697
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
698
        }
699
700
        if (!$storePrimaryMnemonic) {
701
            $primaryMnemonic = false;
702
        }
703
704
        // create primary public key from the created private key
705
        $path = $walletPath->keyIndexPath()->publicPath();
706
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
707
708
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
709
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
710
        }
711
712
        $backupMnemonic = null;
713
        $backupPublicKey = null;
714
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
715
            /** @var HierarchicalKey $backupPrivateKey */
716
            list($backupMnemonic, , ) = $this->newV1BackupSeed();
717
        } else if (isset($options['backup_mnemonic'])) {
718
            $backupMnemonic = $options['backup_mnemonic'];
719
        } elseif (isset($options['backup_public_key'])) {
720
            $backupPublicKey = $options['backup_public_key'];
721
        }
722
723
        if ($backupPublicKey) {
724
            if (is_string($backupPublicKey)) {
725
                $backupPublicKey = [$backupPublicKey, "m"];
726
            }
727
        } else {
728
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
729
            $backupPublicKey = BIP32Key::create($backupPrivateKey->withoutPrivateKey(), "M");
730
        }
731
732
        $btcAddrCreator = new BitcoinAddressCreator();
733
        // create a checksum of our private key which we'll later use to verify we used the right password
734
        $checksum = $primaryPrivateKey->getAddress($btcAddrCreator)->getAddress();
735
        $addressReader = $this->makeAddressReader($options);
736
737
        // send the public keys to the server to store them
738
        //  and the mnemonic, which is safe because it's useless without the password
739
        $data = $this->storeNewWalletV1(
740
            $options['identifier'],
741
            $primaryPublicKey->tuple(),
742
            $backupPublicKey->tuple(),
743
            $primaryMnemonic,
744
            $checksum,
745
            $options['key_index'],
746
            array_key_exists('segwit', $options) ? $options['segwit'] : false
747
        );
748
749
        // received the blocktrail public keys
750
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
751
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
752
        }, $data['blocktrail_public_keys']);
753
754
        $wallet = new WalletV1(
755
            $this,
756
            $options['identifier'],
757
            $primaryMnemonic,
758
            [$options['key_index'] => $primaryPublicKey],
759
            $backupPublicKey,
760
            $blocktrailPublicKeys,
761
            $options['key_index'],
762
            $this->network,
763
            $this->testnet,
764
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
765
            $addressReader,
766
            $checksum
767
        );
768
769
        $wallet->unlock($options);
770
771
        // return wallet and backup mnemonic
772
        return [
773
            $wallet,
774
            [
775
                'primary_mnemonic' => $primaryMnemonic,
776
                'backup_mnemonic' => $backupMnemonic,
777
                'blocktrail_public_keys' => $blocktrailPublicKeys,
778
            ],
779
        ];
780
    }
781
782
    public function randomBits($bits) {
783
        return $this->randomBytes($bits / 8);
784
    }
785
786
    public function randomBytes($bytes) {
787
        return (new Random())->bytes($bytes)->getBinary();
788
    }
789
790
    protected function createNewWalletV2($options) {
791
        $walletPath = WalletPath::create($options['key_index']);
792
793
        if (isset($options['store_primary_mnemonic'])) {
794
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
795
        }
796
797
        if (!isset($options['store_data_on_server'])) {
798
            if (isset($options['primary_private_key'])) {
799
                $options['store_data_on_server'] = false;
800
            } else {
801
                $options['store_data_on_server'] = true;
802
            }
803
        }
804
805
        $storeDataOnServer = $options['store_data_on_server'];
806
807
        $secret = null;
808
        $encryptedSecret = null;
809
        $primarySeed = null;
810
        $encryptedPrimarySeed = null;
811
        $recoverySecret = null;
812
        $recoveryEncryptedSecret = null;
813
        $backupSeed = null;
814
815
        if (!isset($options['primary_private_key'])) {
816
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed();
817
        }
818
819
        if ($storeDataOnServer) {
820
            if (!isset($options['secret'])) {
821
                if (!$options['passphrase']) {
822
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
823
                }
824
825
                list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']);
826
            } else {
827
                $secret = $options['secret'];
828
            }
829
830
            $encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret);
831
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret);
832
        }
833
834
        if (!isset($options['backup_public_key'])) {
835
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed();
836
        }
837
838
        if (isset($options['primary_private_key'])) {
839
            $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...
840
        } else {
841
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
842
        }
843
844
        // create primary public key from the created private key
845
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
846
847
        if (!isset($options['backup_public_key'])) {
848
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
849
        }
850
851
        // create a checksum of our private key which we'll later use to verify we used the right password
852
        $btcAddrCreator = new BitcoinAddressCreator();
853
        $checksum = $options['primary_private_key']->key()->getAddress($btcAddrCreator)->getAddress();
854
        $addressReader = $this->makeAddressReader($options);
855
856
        // send the public keys and encrypted data to server
857
        $data = $this->storeNewWalletV2(
858
            $options['identifier'],
859
            $options['primary_public_key']->tuple(),
860
            $options['backup_public_key']->tuple(),
861
            $storeDataOnServer ? $encryptedPrimarySeed : false,
862
            $storeDataOnServer ? $encryptedSecret : false,
863
            $storeDataOnServer ? $recoverySecret : false,
864
            $checksum,
865
            $options['key_index'],
866
            array_key_exists('segwit', $options) ? $options['segwit'] : false
867
        );
868
869
        // received the blocktrail public keys
870
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
871
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
872
        }, $data['blocktrail_public_keys']);
873
874
        $wallet = new WalletV2(
875
            $this,
876
            $options['identifier'],
877
            $encryptedPrimarySeed,
878
            $encryptedSecret,
879
            [$options['key_index'] => $options['primary_public_key']],
880
            $options['backup_public_key'],
881
            $blocktrailPublicKeys,
882
            $options['key_index'],
883
            $this->network,
884
            $this->testnet,
885
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
886
            $addressReader,
887
            $checksum
888
        );
889
890
        $wallet->unlock([
891
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
892
            'primary_private_key' => $options['primary_private_key'],
893
            'primary_seed' => $primarySeed,
894
            'secret' => $secret,
895
        ]);
896
897
        // return wallet and mnemonics for backup sheet
898
        return [
899
            $wallet,
900
            [
901
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
902
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
903
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
904
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
905
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
906
                    return [$keyIndex, $pubKey->tuple()];
907
                }, $blocktrailPublicKeys),
908
            ],
909
        ];
910
    }
911
912
    protected function createNewWalletV3($options) {
913
        $walletPath = WalletPath::create($options['key_index']);
914
915
        if (isset($options['store_primary_mnemonic'])) {
916
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
917
        }
918
919
        if (!isset($options['store_data_on_server'])) {
920
            if (isset($options['primary_private_key'])) {
921
                $options['store_data_on_server'] = false;
922
            } else {
923
                $options['store_data_on_server'] = true;
924
            }
925
        }
926
927
        $storeDataOnServer = $options['store_data_on_server'];
928
929
        $secret = null;
930
        $encryptedSecret = null;
931
        $primarySeed = null;
932
        $encryptedPrimarySeed = null;
933
        $recoverySecret = null;
934
        $recoveryEncryptedSecret = null;
935
        $backupSeed = null;
936
937
        if (!isset($options['primary_private_key'])) {
938
            if (isset($options['primary_seed'])) {
939
                if (!$options['primary_seed'] instanceof BufferInterface) {
940
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
941
                }
942
                $primarySeed = $options['primary_seed'];
943
            } else {
944
                $primarySeed = $this->newV3PrimarySeed();
945
            }
946
        }
947
948
        if ($storeDataOnServer) {
949
            if (!isset($options['secret'])) {
950
                if (!$options['passphrase']) {
951
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
952
                }
953
954
                list($secret, $encryptedSecret) = $this->newV3Secret($options['passphrase']);
955
            } else {
956
                if (!$options['secret'] instanceof Buffer) {
957
                    throw new \InvalidArgumentException('Secret must be provided as a Buffer');
958
                }
959
960
                $secret = $options['secret'];
961
            }
962
963
            $encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret);
964
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret);
965
        }
966
967
        if (!isset($options['backup_public_key'])) {
968
            if (isset($options['backup_seed'])) {
969
                if (!$options['backup_seed'] instanceof Buffer) {
970
                    throw new \InvalidArgumentException('Backup seed must be an instance of Buffer');
971
                }
972
                $backupSeed = $options['backup_seed'];
973
            } else {
974
                $backupSeed = $this->newV3BackupSeed();
975
            }
976
        }
977
978
        if (isset($options['primary_private_key'])) {
979
            $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...
980
        } else {
981
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
982
        }
983
984
        // create primary public key from the created private key
985
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
986
987
        if (!isset($options['backup_public_key'])) {
988
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
989
        }
990
991
        // create a checksum of our private key which we'll later use to verify we used the right password
992
        $btcAddrCreator = new BitcoinAddressCreator();
993
        $checksum = $options['primary_private_key']->key()->getAddress($btcAddrCreator)->getAddress();
994
        $addressReader = $this->makeAddressReader($options);
995
996
        // send the public keys and encrypted data to server
997
        $data = $this->storeNewWalletV3(
998
            $options['identifier'],
999
            $options['primary_public_key']->tuple(),
1000
            $options['backup_public_key']->tuple(),
1001
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
1002
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
1003
            $storeDataOnServer ? $recoverySecret->getHex() : false,
1004
            $checksum,
1005
            $options['key_index'],
1006
            array_key_exists('segwit', $options) ? $options['segwit'] : false
1007
        );
1008
1009
        // received the blocktrail public keys
1010
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
1011
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
1012
        }, $data['blocktrail_public_keys']);
1013
1014
        $wallet = new WalletV3(
1015
            $this,
1016
            $options['identifier'],
1017
            $encryptedPrimarySeed,
1018
            $encryptedSecret,
1019
            [$options['key_index'] => $options['primary_public_key']],
1020
            $options['backup_public_key'],
1021
            $blocktrailPublicKeys,
1022
            $options['key_index'],
1023
            $this->network,
1024
            $this->testnet,
1025
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1026
            $addressReader,
1027
            $checksum
1028
        );
1029
1030
        $wallet->unlock([
1031
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1032
            'primary_private_key' => $options['primary_private_key'],
1033
            'primary_seed' => $primarySeed,
1034
            'secret' => $secret,
1035
        ]);
1036
1037
        // return wallet and mnemonics for backup sheet
1038
        return [
1039
            $wallet,
1040
            [
1041
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1042
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1043
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1044
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1045
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1046
                    return [$keyIndex, $pubKey->tuple()];
1047
                }, $blocktrailPublicKeys),
1048
            ]
1049
        ];
1050
    }
1051
1052
    public function newV2PrimarySeed() {
1053
        return $this->randomBits(256);
1054
    }
1055
1056
    public function newV2BackupSeed() {
1057 3
        return $this->randomBits(256);
1058 3
    }
1059 3
1060
    public function newV2Secret($passphrase) {
1061
        $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
1062
        $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase);
1063 3
1064
        return [$secret, $encryptedSecret];
1065
    }
1066 3
1067
    public function newV2EncryptedPrimarySeed($primarySeed, $secret) {
1068
        return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
1069
    }
1070
1071
    public function newV2RecoverySecret($secret) {
1072 3
        $recoverySecret = bin2hex($this->randomBits(256));
1073 3
        $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
1074 3
1075 3
        return [$recoverySecret, $recoveryEncryptedSecret];
1076
    }
1077
1078
    public function newV3PrimarySeed() {
1079
        return new Buffer($this->randomBits(256));
1080
    }
1081
1082
    public function newV3BackupSeed() {
1083
        return new Buffer($this->randomBits(256));
1084
    }
1085
1086
    public function newV3Secret($passphrase) {
1087
        $secret = new Buffer($this->randomBits(256));
1088
        $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
1089
            ->getBuffer();
1090
1091
        return [$secret, $encryptedSecret];
1092
    }
1093
1094
    public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
1095
        return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
1096
            ->getBuffer();
1097
    }
1098
1099
    public function newV3RecoverySecret(Buffer $secret) {
1100
        $recoverySecret = new Buffer($this->randomBits(256));
1101
        $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
1102
            ->getBuffer();
1103
1104
        return [$recoverySecret, $recoveryEncryptedSecret];
1105
    }
1106
1107
    /**
1108
     * @param array $bip32Key
1109
     * @throws BlocktrailSDKException
1110
     */
1111
    private function verifyPublicBIP32Key(array $bip32Key) {
1112
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1113
        if ($hk->isPrivate()) {
1114
            throw new BlocktrailSDKException('Private key was included in request, abort');
1115
        }
1116
1117
        if (substr($bip32Key[1], 0, 1) === "m") {
1118
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1119 3
        }
1120
    }
1121 3
1122
    /**
1123 3
     * @param array $walletData
1124 3
     * @throws BlocktrailSDKException
1125 3
     */
1126 3
    private function verifyPublicOnly(array $walletData) {
1127 3
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1128 3
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1129 3
    }
1130 3
1131
    /**
1132 3
     * create wallet using the API
1133 3
     *
1134
     * @param string    $identifier             the wallet identifier to create
1135
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1136
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1137
     * @param string    $primaryMnemonic        mnemonic to store
1138
     * @param string    $checksum               checksum to store
1139
     * @param int       $keyIndex               account that we expect to use
1140
     * @param bool      $segwit                 opt in to segwit
1141
     * @return mixed
1142
     */
1143
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1144
        $data = [
1145
            'identifier' => $identifier,
1146
            'primary_public_key' => $primaryPublicKey,
1147
            'backup_public_key' => $backupPublicKey,
1148
            'primary_mnemonic' => $primaryMnemonic,
1149
            'checksum' => $checksum,
1150
            'key_index' => $keyIndex,
1151
            'segwit' => $segwit,
1152
        ];
1153
        $this->verifyPublicOnly($data);
1154
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1155
        return self::jsonDecode($response->body(), true);
1156
    }
1157
1158
    /**
1159
     * create wallet using the API
1160
     *
1161
     * @param string $identifier       the wallet identifier to create
1162
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1163
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1164
     * @param        $encryptedPrimarySeed
1165
     * @param        $encryptedSecret
1166
     * @param        $recoverySecret
1167
     * @param string $checksum         checksum to store
1168
     * @param int    $keyIndex         account that we expect to use
1169
     * @param bool   $segwit           opt in to segwit
1170
     * @return mixed
1171
     * @throws \Exception
1172
     */
1173
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1174
        $data = [
1175
            'identifier' => $identifier,
1176
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1177
            'primary_public_key' => $primaryPublicKey,
1178
            'backup_public_key' => $backupPublicKey,
1179
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1180
            'encrypted_secret' => $encryptedSecret,
1181
            'recovery_secret' => $recoverySecret,
1182
            'checksum' => $checksum,
1183
            'key_index' => $keyIndex,
1184
            'segwit' => $segwit,
1185
        ];
1186
        $this->verifyPublicOnly($data);
1187
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1188
        return self::jsonDecode($response->body(), true);
1189
    }
1190
1191
    /**
1192
     * create wallet using the API
1193
     *
1194
     * @param string $identifier       the wallet identifier to create
1195
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1196
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1197
     * @param        $encryptedPrimarySeed
1198
     * @param        $encryptedSecret
1199
     * @param        $recoverySecret
1200
     * @param string $checksum         checksum to store
1201
     * @param int    $keyIndex         account that we expect to use
1202
     * @param bool   $segwit           opt in to segwit
1203
     * @return mixed
1204
     * @throws \Exception
1205
     */
1206
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1207
1208
        $data = [
1209
            'identifier' => $identifier,
1210
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1211
            'primary_public_key' => $primaryPublicKey,
1212
            'backup_public_key' => $backupPublicKey,
1213
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1214
            'encrypted_secret' => $encryptedSecret,
1215
            'recovery_secret' => $recoverySecret,
1216
            'checksum' => $checksum,
1217
            'key_index' => $keyIndex,
1218
            'segwit' => $segwit,
1219
        ];
1220
1221
        $this->verifyPublicOnly($data);
1222
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1223
        return self::jsonDecode($response->body(), true);
1224
    }
1225
1226
    /**
1227
     * upgrade wallet to use a new account number
1228
     *  the account number specifies which blocktrail cosigning key is used
1229
     *
1230 23
     * @param string    $identifier             the wallet identifier to be upgraded
1231 23
     * @param int       $keyIndex               the new account to use
1232 1
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1233
     * @return mixed
1234 1
     */
1235 1
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1236
        $data = [
1237
            'key_index' => $keyIndex,
1238
            'primary_public_key' => $primaryPublicKey
1239 23
        ];
1240 23
1241 23
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1242 23
        return self::jsonDecode($response->body(), true);
1243 23
    }
1244
1245
    /**
1246 23
     * @param array $options
1247
     * @return BaseAddressCreator
1248
     */
1249
    private function makeAddressReader(array $options) {
1250
        if ($this->network == "bitcoincash") {
1251
            $useCashAddress = false;
1252
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1253
                $useCashAddress = true;
1254
            }
1255
            return new BitcoinCashAddressCreator($useCashAddress);
1256
        } else {
1257
            return new BitcoinAddressCreator();
1258
        }
1259
    }
1260
1261
    /**
1262
     * initialize a previously created wallet
1263
     *
1264
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1265
     *
1266
     * Some of the options:
1267
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1268
     *    so the wallet is loaded in read-only mode (no private key)
1269
     *  - "check_backup_key" can be set to your own backup key:
1270
     *    Format: ["M', "xpub..."]
1271
     *    Setting this will allow the SDK to check the server hasn't
1272
     *    a different key (one it happens to control)
1273
1274
     * Either takes one argument:
1275
     * @param array $options
1276
     *
1277
     * Or takes two arguments (old, deprecated syntax):
1278
     * (@nonPHP-doc) @param string    $identifier             the wallet identifier to be initialized
1279
     * (@nonPHP-doc) @param string    $password               the password to decrypt the mnemonic with
1280
     *
1281
     * @return WalletInterface
1282
     * @throws \Exception
1283
     */
1284
    public function initWallet($options) {
1285
        if (!is_array($options)) {
1286
            $args = func_get_args();
1287
            $options = [
1288
                "identifier" => $args[0],
1289
                "password" => $args[1],
1290
            ];
1291
        }
1292
1293
        $identifier = $options['identifier'];
1294
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1295
                    (isset($options['readOnly']) ? $options['readOnly'] :
1296
                        (isset($options['read-only']) ? $options['read-only'] :
1297
                            false));
1298
1299
        // get the wallet data from the server
1300
        $data = $this->getWallet($identifier);
1301
        if (!$data) {
1302
            throw new \Exception("Failed to get wallet");
1303
        }
1304
1305
        if (array_key_exists('check_backup_key', $options)) {
1306
            if (!is_string($options['check_backup_key'])) {
1307
                throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
1308
            }
1309
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1310
                throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
1311
            }
1312
        }
1313
1314
        $addressReader = $this->makeAddressReader($options);
1315
1316
        switch ($data['wallet_version']) {
1317
            case Wallet::WALLET_VERSION_V1:
1318
                $wallet = new WalletV1(
1319
                    $this,
1320
                    $identifier,
1321
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1322
                    $data['primary_public_keys'],
1323
                    $data['backup_public_key'],
1324
                    $data['blocktrail_public_keys'],
1325
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1326
                    $this->network,
1327
                    $this->testnet,
1328
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1329
                    $addressReader,
1330
                    $data['checksum']
1331
                );
1332
                break;
1333
            case Wallet::WALLET_VERSION_V2:
1334
                $wallet = new WalletV2(
1335
                    $this,
1336
                    $identifier,
1337
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1338
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1339
                    $data['primary_public_keys'],
1340
                    $data['backup_public_key'],
1341
                    $data['blocktrail_public_keys'],
1342
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1343
                    $this->network,
1344
                    $this->testnet,
1345
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1346
                    $addressReader,
1347
                    $data['checksum']
1348
                );
1349 23
                break;
1350 23
            case Wallet::WALLET_VERSION_V3:
1351
                if (isset($options['encrypted_primary_seed'])) {
1352
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1353
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1354
                    }
1355
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1356
                } else {
1357
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1358
                }
1359
1360
                if (isset($options['encrypted_secret'])) {
1361
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1362
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1363
                    }
1364
1365
                    $encryptedSecret = $data['encrypted_secret'];
1366
                } else {
1367
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1368
                }
1369
1370
                $wallet = new WalletV3(
1371
                    $this,
1372
                    $identifier,
1373
                    $encryptedPrimarySeed,
1374
                    $encryptedSecret,
1375
                    $data['primary_public_keys'],
1376
                    $data['backup_public_key'],
1377
                    $data['blocktrail_public_keys'],
1378
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1379
                    $this->network,
1380
                    $this->testnet,
1381
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1382
                    $addressReader,
1383
                    $data['checksum']
1384
                );
1385
                break;
1386
            default:
1387
                throw new \InvalidArgumentException("Invalid wallet version");
1388
        }
1389
1390
        if (!$readonly) {
1391
            $wallet->unlock($options);
1392
        }
1393
1394
        return $wallet;
1395
    }
1396
1397
    /**
1398
     * get the wallet data from the server
1399
     *
1400
     * @param string    $identifier             the identifier of the wallet
1401
     * @return mixed
1402
     */
1403
    public function getWallet($identifier) {
1404
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1405
        return self::jsonDecode($response->body(), true);
1406
    }
1407
1408
    /**
1409
     * update the wallet data on the server
1410
     *
1411
     * @param string    $identifier
1412
     * @param $data
1413
     * @return mixed
1414
     */
1415
    public function updateWallet($identifier, $data) {
1416
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1417
        return self::jsonDecode($response->body(), true);
1418
    }
1419
1420
    /**
1421
     * delete a wallet from the server
1422
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1423
     *  is required to be able to delete a wallet
1424
     *
1425
     * @param string    $identifier             the identifier of the wallet
1426
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1427
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1428
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1429
     * @return mixed
1430
     */
1431
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1432
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1433
            'checksum' => $checksumAddress,
1434
            'signature' => $signature
1435
        ], RestClient::AUTH_HTTP_SIG, 360);
1436
        return self::jsonDecode($response->body(), true);
1437
    }
1438
1439
    /**
1440
     * create new backup key;
1441
     *  1) a BIP39 mnemonic
1442
     *  2) a seed from that mnemonic with a blank password
1443
     *  3) a private key from that seed
1444
     *
1445
     * @return array [mnemonic, seed, key]
1446
     */
1447
    protected function newV1BackupSeed() {
1448
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1449
1450
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1451
    }
1452
1453
    /**
1454
     * create new primary key;
1455
     *  1) a BIP39 mnemonic
1456
     *  2) a seed from that mnemonic with the password
1457
     *  3) a private key from that seed
1458
     *
1459
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1460
     * @return array [mnemonic, seed, key]
1461
     * @TODO: require a strong password?
1462
     */
1463
    protected function newV1PrimarySeed($passphrase) {
1464
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1465
1466
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1467
    }
1468
1469
    /**
1470
     * create a new key;
1471
     *  1) a BIP39 mnemonic
1472
     *  2) a seed from that mnemonic with the password
1473
     *  3) a private key from that seed
1474
     *
1475
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1476
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1477
     * @return array
1478
     */
1479
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1480
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1481
        do {
1482
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1483
1484
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1485
1486
            $key = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
1487
            try {
1488
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1489
            } catch (\Exception $e) {
1490
                // try again
1491
            }
1492
        } while (!$key);
1493
1494
        return [$mnemonic, $seed, $key];
1495
    }
1496
1497
    /**
1498
     * generate a new mnemonic from some random entropy (512 bit)
1499
     *
1500
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1501
     * @return string
1502
     * @throws \Exception
1503
     */
1504
    public function generateNewMnemonic($forceEntropy = null) {
1505
        if ($forceEntropy === null) {
1506
            $random = new Random();
1507
            $entropy = $random->bytes(512 / 8);
1508
        } else {
1509
            $entropy = $forceEntropy;
1510
        }
1511
1512
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy can also be of type string; however, parameter $entropy of BitWasp\Bitcoin\Mnemonic...ic::entropyToMnemonic() does only seem to accept BitWasp\Buffertools\BufferInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1512
        return MnemonicFactory::bip39()->entropyToMnemonic(/** @scrutinizer ignore-type */ $entropy);
Loading history...
1513
    }
1514
1515
    /**
1516
     * get the balance for the wallet
1517
     *
1518
     * @param string    $identifier             the identifier of the wallet
1519
     * @return array
1520
     */
1521
    public function getWalletBalance($identifier) {
1522
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1523
        return self::jsonDecode($response->body(), true);
1524
    }
1525
1526
    /**
1527
     * get a new derivation number for specified parent path
1528
     *  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
1529
     *
1530
     * returns the path
1531
     *
1532
     * @param string    $identifier             the identifier of the wallet
1533
     * @param string    $path                   the parent path for which to get a new derivation
1534
     * @return string
1535
     */
1536
    public function getNewDerivation($identifier, $path) {
1537
        $result = $this->_getNewDerivation($identifier, $path);
1538
        return $result['path'];
1539
    }
1540
1541
    /**
1542
     * get a new derivation number for specified parent path
1543
     *  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
1544
     *
1545
     * @param string    $identifier             the identifier of the wallet
1546
     * @param string    $path                   the parent path for which to get a new derivation
1547
     * @return mixed
1548
     */
1549
    public function _getNewDerivation($identifier, $path) {
1550
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1551
        return self::jsonDecode($response->body(), true);
1552
    }
1553
1554
    /**
1555
     * get the path (and redeemScript) to specified address
1556
     *
1557
     * @param string $identifier
1558
     * @param string $address
1559
     * @return array
1560
     * @throws \Exception
1561
     */
1562
    public function getPathForAddress($identifier, $address) {
1563
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1564
        return self::jsonDecode($response->body(), true)['path'];
1565
    }
1566
1567
    /**
1568
     * send the transaction using the API
1569
     *
1570
     * @param string       $identifier     the identifier of the wallet
1571
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1572
     * @param array        $paths          list of the paths that were used for the UTXO
1573
     * @param bool         $checkFee       let the server verify the fee after signing
1574
     * @param null         $twoFactorToken
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $twoFactorToken is correct as it would always require null to be passed?
Loading history...
1575
     * @return string                                the complete raw transaction
1576
     * @throws \Exception
1577
     */
1578
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1579
        $data = [
1580
            'paths' => $paths,
1581
            'two_factor_token' => $twoFactorToken,
1582
        ];
1583
1584
        if (is_array($rawTransaction)) {
1585
            if (array_key_exists('base_transaction', $rawTransaction)
1586
            && array_key_exists('signed_transaction', $rawTransaction)) {
1587
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1588
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1589
            } else {
1590
                throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1591
            }
1592
        } else {
1593
            $data['raw_transaction'] = $rawTransaction;
1594
        }
1595
1596
        // dynamic TTL for when we're signing really big transactions
1597
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1598
1599
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1600
        $signed = self::jsonDecode($response->body(), true);
1601
1602
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1603
            throw new \Exception("Failed to completely sign transaction");
1604
        }
1605
1606
        // create TX hash from the raw signed hex
1607
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1608
    }
1609
1610
    /**
1611
     * use the API to get the best inputs to use based on the outputs
1612
     *
1613
     * the return array has the following format:
1614
     * [
1615
     *  "utxos" => [
1616
     *      [
1617
     *          "hash" => "<txHash>",
1618
     *          "idx" => "<index of the output of that <txHash>",
1619
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1620
     *          "value" => 32746327,
1621
     *          "address" => "1address",
1622
     *          "path" => "m/44'/1'/0'/0/13",
1623
     *          "redeem_script" => "<redeemScript-hex>",
1624
     *      ],
1625
     *  ],
1626
     *  "fee"   => 10000,
1627
     *  "change"=> 1010109201,
1628
     * ]
1629
     *
1630
     * @param string   $identifier              the identifier of the wallet
1631
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1632
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1633
     *                                          so you have some time to spend them without race-conditions
1634
     * @param bool     $allowZeroConf
1635
     * @param string   $feeStrategy
1636
     * @param null|int $forceFee
1637
     * @return array
1638
     * @throws \Exception
1639
     */
1640
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1641
        $args = [
1642
            'lock' => (int)!!$lockUTXO,
1643
            'zeroconf' => (int)!!$allowZeroConf,
1644
            'fee_strategy' => $feeStrategy,
1645
        ];
1646
1647
        if ($forceFee !== null) {
1648
            $args['forcefee'] = (int)$forceFee;
1649
        }
1650 1
1651 1
        $response = $this->blocktrailClient->post(
1652
            "wallet/{$identifier}/coin-selection",
1653
            $args,
1654
            $outputs,
1655
            RestClient::AUTH_HTTP_SIG
1656
        );
1657
1658
        \var_export(self::jsonDecode($response->body(), true));
1659
1660
        return self::jsonDecode($response->body(), true);
1661
    }
1662
1663
    /**
1664
     *
1665
     * @param string   $identifier the identifier of the wallet
1666
     * @param bool     $allowZeroConf
1667
     * @param string   $feeStrategy
1668
     * @param null|int $forceFee
1669
     * @param int      $outputCnt
1670
     * @return array
1671
     * @throws \Exception
1672
     */
1673
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1674
        $args = [
1675
            'zeroconf' => (int)!!$allowZeroConf,
1676
            'fee_strategy' => $feeStrategy,
1677
            'outputs' => $outputCnt,
1678
        ];
1679
1680
        if ($forceFee !== null) {
1681
            $args['forcefee'] = (int)$forceFee;
1682
        }
1683
1684
        $response = $this->blocktrailClient->get(
1685
            "wallet/{$identifier}/max-spendable",
1686
            $args,
1687
            RestClient::AUTH_HTTP_SIG
1688
        );
1689
1690
        return self::jsonDecode($response->body(), true);
1691
    }
1692
1693
    /**
1694
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1695
     */
1696
    public function feePerKB() {
1697
        $response = $this->blocktrailClient->get("fee-per-kb");
1698
        return self::jsonDecode($response->body(), true);
1699
    }
1700
1701
    /**
1702
     * get the current price index
1703
     *
1704
     * @return array        eg; ['USD' => 287.30]
1705
     */
1706
    public function price() {
1707
        $response = $this->blocktrailClient->get("price");
1708
        return self::jsonDecode($response->body(), true);
1709
    }
1710
1711
    /**
1712
     * setup webhook for wallet
1713
     *
1714
     * @param string    $identifier         the wallet identifier for which to create the webhook
1715
     * @param string    $webhookIdentifier  the webhook identifier to use
1716
     * @param string    $url                the url to receive the webhook events
1717
     * @return array
1718
     */
1719
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1720
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1721
        return self::jsonDecode($response->body(), true);
1722
    }
1723
1724
    /**
1725
     * delete webhook for wallet
1726
     *
1727
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1728
     * @param string    $webhookIdentifier  the webhook identifier to delete
1729
     * @return array
1730
     */
1731
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1732
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1733
        return self::jsonDecode($response->body(), true);
1734
    }
1735
1736
    /**
1737
     * lock a specific unspent output
1738
     *
1739
     * @param     $identifier
1740
     * @param     $txHash
1741
     * @param     $txIdx
1742
     * @param int $ttl
1743
     * @return bool
1744
     */
1745
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1746
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1747
        return self::jsonDecode($response->body(), true)['locked'];
1748
    }
1749
1750
    /**
1751
     * unlock a specific unspent output
1752
     *
1753
     * @param     $identifier
1754
     * @param     $txHash
1755
     * @param     $txIdx
1756
     * @return bool
1757
     */
1758
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1759
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1760
        return self::jsonDecode($response->body(), true)['unlocked'];
1761
    }
1762
1763
    /**
1764
     * get all transactions for wallet (paginated)
1765
     *
1766
     * @param  string  $identifier  the wallet identifier for which to get transactions
1767
     * @param  integer $page        pagination: page number
1768
     * @param  integer $limit       pagination: records per page (max 500)
1769
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1770
     * @return array                associative array containing the response
1771
     */
1772
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1773
        $queryString = [
1774
            'page' => $page,
1775
            'limit' => $limit,
1776
            'sort_dir' => $sortDir
1777
        ];
1778
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1779
        return self::jsonDecode($response->body(), true);
1780
    }
1781
1782
    /**
1783
     * get all addresses for wallet (paginated)
1784
     *
1785
     * @param  string  $identifier  the wallet identifier for which to get addresses
1786
     * @param  integer $page        pagination: page number
1787
     * @param  integer $limit       pagination: records per page (max 500)
1788
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1789
     * @return array                associative array containing the response
1790
     */
1791
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1792
        $queryString = [
1793
            'page' => $page,
1794
            'limit' => $limit,
1795
            'sort_dir' => $sortDir
1796
        ];
1797
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1798
        return self::jsonDecode($response->body(), true);
1799
    }
1800
1801
    /**
1802
     * get all UTXOs for wallet (paginated)
1803
     *
1804
     * @param  string  $identifier  the wallet identifier for which to get addresses
1805
     * @param  integer $page        pagination: page number
1806
     * @param  integer $limit       pagination: records per page (max 500)
1807
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1808
     * @param  boolean $zeroconf    include zero confirmation transactions
1809
     * @return array                associative array containing the response
1810
     */
1811
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1812
        $queryString = [
1813
            'page' => $page,
1814
            'limit' => $limit,
1815
            'sort_dir' => $sortDir,
1816
            'zeroconf' => (int)!!$zeroconf,
1817
        ];
1818
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1819
        return self::jsonDecode($response->body(), true);
1820
    }
1821
1822
    /**
1823
     * get a paginated list of all wallets associated with the api user
1824
     *
1825
     * @param  integer          $page    pagination: page number
1826
     * @param  integer          $limit   pagination: records per page
1827
     * @return array                     associative array containing the response
1828
     */
1829
    public function allWallets($page = 1, $limit = 20) {
1830
        $queryString = [
1831 2
            'page' => $page,
1832 2
            'limit' => $limit
1833 2
        ];
1834 2
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1835
        return self::jsonDecode($response->body(), true);
1836
    }
1837
1838
    /**
1839 2
     * send raw transaction
1840 2
     *
1841
     * @param     $txHex
1842 2
     * @return bool
1843 2
     */
1844
    public function sendRawTransaction($txHex) {
1845
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1846
        return self::jsonDecode($response->body(), true);
1847
    }
1848
1849
    /**
1850
     * testnet only ;-)
1851
     *
1852
     * @param     $address
1853
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1854
     * @return mixed
1855
     * @throws \Exception
1856
     */
1857
    public function faucetWithdrawal($address, $amount = 10000) {
1858
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1859
            'address' => $address,
1860
            'amount' => $amount,
1861
        ], RestClient::AUTH_HTTP_SIG);
1862
        return self::jsonDecode($response->body(), true);
1863
    }
1864
1865
    /**
1866
     * Exists for BC. Remove at major bump.
1867
     *
1868
     * @see faucetWithdrawal
1869
     * @deprecated
1870
     * @param     $address
1871
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1872
     * @return mixed
1873
     * @throws \Exception
1874
     */
1875
    public function faucetWithdrawl($address, $amount = 10000) {
1876
        return $this->faucetWithdrawal($address, $amount);
1877
    }
1878 1
1879 1
    /**
1880
     * verify a message signed bitcoin-core style
1881
     *
1882
     * @param  string           $message
1883
     * @param  string           $address
1884
     * @param  string           $signature
1885
     * @return boolean
1886
     */
1887
    public function verifyMessage($message, $address, $signature) {
1888
        $adapter = Bitcoin::getEcAdapter();
1889
        $btcAddrCreator = new BitcoinAddressCreator();
1890
        $addr = $btcAddrCreator->fromString($address);
1891
        if (!$addr instanceof PayToPubKeyHashAddress) {
1892
            throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
1893
        }
1894
1895
        /** @var CompactSignatureSerializerInterface $csSerializer */
1896
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Bug introduced by
$adapter of type BitWasp\Bitcoin\Crypto\E...pter\EcAdapterInterface is incompatible with the type boolean expected by parameter $useCache of BitWasp\Bitcoin\Crypto\E...alizer::getSerializer(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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