Completed
Branch master (f66fc6)
by
unknown
07:47
created

Wallet::getMaxSpendable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 4
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\BitcoinLib\BIP32;
6
use BitWasp\BitcoinLib\BIP39\BIP39;
7
use BitWasp\BitcoinLib\BitcoinLib;
8
use BitWasp\BitcoinLib\RawTransaction;
9
use Blocktrail\SDK\Bitcoin\BIP32Key;
10
use Blocktrail\SDK\Bitcoin\BIP32Path;
11
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
12
13
/**
14
 * Class Wallet
15
 */
16
class Wallet implements WalletInterface {
17
18
    const BASE_FEE = 10000;
19
20
    /**
21
     * development / debug setting
22
     *  when getting a new derivation from the API,
23
     *  will verify address / redeeemScript with the values the API provides
24
     */
25
    const VERIFY_NEW_DERIVATION = true;
26
27
    /**
28
     * @var BlocktrailSDKInterface
29
     */
30
    protected $sdk;
31
32
    /**
33
     * @var string
34
     */
35
    protected $identifier;
36
37
    /**
38
     * BIP39 Mnemonic for the master primary private key
39
     *
40
     * @var string
41
     */
42
    protected $primaryMnemonic;
43
44
    /**
45
     * BIP32 master primary private key (m/)
46
     *
47
     * @var BIP32Key
48
     */
49
    protected $primaryPrivateKey;
50
51
    /**
52
     * @var BIP32Key[]
53
     */
54
    protected $primaryPublicKeys;
55
56
    /**
57
     * BIP32 master backup public key (M/)
58
59
     * @var BIP32Key
60
     */
61
    protected $backupPublicKey;
62
63
    /**
64
     * map of blocktrail BIP32 public keys
65
     *  keyed by key index
66
     *  path should be `M / key_index'`
67
     *
68
     * @var BIP32Key[]
69
     */
70
    protected $blocktrailPublicKeys;
71
72
    /**
73
     * the 'Blocktrail Key Index' that is used for new addresses
74
     *
75
     * @var int
76
     */
77
    protected $keyIndex;
78
79
    /**
80
     * 'bitcoin'
81
     *
82
     * @var string
83
     */
84
    protected $network;
85
86
    /**
87
     * testnet yes / no
88
     *
89
     * @var bool
90
     */
91
    protected $testnet;
92
93
    /**
94
     * cache of public keys, by path
95
     *
96
     * @var BIP32Key[]
97
     */
98
    protected $pubKeys = [];
99
100
    /**
101
     * cache of address / redeemScript, by path
102
     *
103
     * @var string[][]      [[address, redeemScript)], ]
104
     */
105
    protected $derivations = [];
106
107
    /**
108
     * reverse cache of paths by address
109
     *
110
     * @var string[]
111
     */
112
    protected $derivationsByAddress = [];
113
114
    /**
115
     * @var WalletPath
116
     */
117
    protected $walletPath;
118
119
    private $checksum;
120
121
    private $locked = true;
122
123
    protected $optimalFeePerKB;
124
    protected $lowPriorityFeePerKB;
125
    protected $feePerKBAge;
126
127
    /**
128
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
129
     * @param string                        $identifier                 identifier of the wallet
130
     * @param string                        $primaryMnemonic
131
     * @param array[string, string]         $primaryPublicKeys
132
     * @param array[string, string]         $backupPublicKey            should be BIP32 master public key M/
133
     * @param array[array[string, string]]  $blocktrailPublicKeys
134
     * @param int                           $keyIndex
135
     * @param string                        $network
136
     * @param bool                          $testnet
137
     * @param string                        $checksum
138
     */
139
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, $primaryMnemonic, $primaryPublicKeys, $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) {
140
        $this->sdk = $sdk;
141
142
        $this->identifier = $identifier;
143
144
        $this->primaryMnemonic = $primaryMnemonic;
145
        $this->backupPublicKey = BIP32Key::create($backupPublicKey);
146
        $this->primaryPublicKeys = array_map(function ($key) {
147
            return BIP32Key::create($key);
148
        }, $primaryPublicKeys);
149
        $this->blocktrailPublicKeys = array_map(function ($key) {
150
            return BIP32Key::create($key);
151
        }, $blocktrailPublicKeys);
152
153
        $this->network = $network;
154
        $this->testnet = $testnet;
155
        $this->keyIndex = $keyIndex;
156
        $this->checksum = $checksum;
157
158
        $this->walletPath = WalletPath::create($this->keyIndex);
159
    }
160
161
    /**
162
     * return the wallet identifier
163
     *
164
     * @return string
165
     */
166
    public function getIdentifier() {
167
        return $this->identifier;
168
    }
169
170
    /**
171
     * return the wallet primary mnemonic (for backup purposes)
172
     *
173
     * @return string
174
     */
175
    public function getPrimaryMnemonic() {
176
        return $this->primaryMnemonic;
177
    }
178
179
    /**
180
     * return list of Blocktrail co-sign extended public keys
181
     *
182
     * @return array[]      [ [xpub, path] ]
183
     */
184
    public function getBlocktrailPublicKeys() {
185
        return array_map(function (BIP32Key $key) {
186
            return $key->tuple();
187
        }, $this->blocktrailPublicKeys);
188
    }
189
190
    /**
191
     * unlock wallet so it can be used for payments
192
     *
193
     * @param          $options ['primary_private_key' => key] OR ['passphrase' => pass]
194
     * @param callable $fn
195
     * @return bool
196
     * @throws \Exception
197
     */
198
    public function unlock($options, callable $fn = null) {
199
        // explode the wallet data
200
        $password = isset($options['passphrase']) ? $options['passphrase'] : (isset($options['password']) ? $options['password'] : null);
201
        $primaryMnemonic = $this->primaryMnemonic;
202
        $primaryPrivateKey = isset($options['primary_private_key']) ? $options['primary_private_key'] : null;
203
204
        if ($primaryMnemonic && $primaryPrivateKey) {
205
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
206
        }
207
208
        if (!$primaryMnemonic && !$primaryPrivateKey) {
209
            throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic or Primary PrivateKey");
210
        }
211
212
        if ($primaryMnemonic && !$password) {
213
            throw new \InvalidArgumentException("Can't init wallet with Primary Mnemonic without a passphrase");
214
        }
215
216 View Code Duplication
        if ($primaryPrivateKey) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
            if (is_string($primaryPrivateKey)) {
218
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
219
            }
220
        } else {
221
            // convert the mnemonic to a seed using BIP39 standard
222
            $primarySeed = BIP39::mnemonicToSeedHex($primaryMnemonic, $password);
223
            // create BIP32 private key from the seed
224
            $primaryPrivateKey = BIP32::master_key($primarySeed, $this->network, $this->testnet);
0 ignored issues
show
Bug introduced by
It seems like $primarySeed defined by \BitWasp\BitcoinLib\BIP3...aryMnemonic, $password) on line 222 can also be of type false or null; however, BitWasp\BitcoinLib\BIP32::master_key() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
225
        }
226
227
        $this->primaryPrivateKey = BIP32Key::create($primaryPrivateKey);
228
229
        // create checksum (address) of the primary privatekey to compare to the stored checksum
230
        $checksum = BIP32::key_to_address($primaryPrivateKey[0]);
231
        if ($checksum != $this->checksum) {
232
            throw new \Exception("Checksum [{$checksum}] does not match [{$this->checksum}], most likely due to incorrect password");
233
        }
234
235
        $this->locked = false;
236
237
        // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
238
        if (isset($data['upgrade_key_index'])) {
0 ignored issues
show
Bug introduced by
The variable $data seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
239
            $this->upgradeKeyIndex($data['upgrade_key_index']);
240
        }
241
242
        if ($fn) {
243
            $fn($this);
244
            $this->lock();
245
        }
246
    }
247
248
    /**
249
     * lock the wallet (unsets primary private key)
250
     *
251
     * @return void
252
     */
253
    public function lock() {
254
        $this->primaryPrivateKey = null;
255
        $this->locked = true;
256
    }
257
258
    /**
259
     * check if wallet is locked
260
     *
261
     * @return bool
262
     */
263
    public function isLocked() {
264
        return $this->locked;
265
    }
266
267
    /**
268
     * upgrade wallet to different blocktrail cosign key
269
     *
270
     * @param $keyIndex
271
     * @return bool
272
     * @throws \Exception
273
     */
274
    public function upgradeKeyIndex($keyIndex) {
275
        if ($this->locked) {
276
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
277
        }
278
279
        $walletPath = WalletPath::create($keyIndex);
280
281
        // do the upgrade to the new 'key_index'
282
        $primaryPublicKey = BIP32::extended_private_to_public(BIP32::build_key($this->primaryPrivateKey->tuple(), (string)$walletPath->keyIndexPath()));
0 ignored issues
show
Documentation introduced by
$this->primaryPrivateKey->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
283
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey);
0 ignored issues
show
Bug introduced by
It seems like $primaryPublicKey defined by \BitWasp\BitcoinLib\BIP3...tPath->keyIndexPath())) on line 282 can also be of type string; however, Blocktrail\SDK\Blocktrai...face::upgradeKeyIndex() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
284
285
        $this->primaryPublicKeys[$keyIndex] = BIP32Key::create($primaryPublicKey);
286
287
        $this->keyIndex = $keyIndex;
288
        $this->walletPath = $walletPath;
289
290
        // update the blocktrail public keys
291
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
292
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
293
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create($pubKey);
294
            }
295
        }
296
297
        return true;
298
    }
299
300
    /**
301
     * get a new BIP32 derivation for the next (unused) address
302
     *  by requesting it from the API
303
     *
304
     * @return string
305
     * @throws \Exception
306
     */
307
    protected function getNewDerivation() {
308
        $path = $this->walletPath->path()->last("*");
309
310
        if (self::VERIFY_NEW_DERIVATION) {
311
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
312
313
            $path = $new['path'];
314
            $address = $new['address'];
315
            $redeemScript = $new['redeem_script'];
316
317
            list($checkAddress, $checkRedeemScript) = $this->getRedeemScriptByPath($path);
318
319
            if ($checkAddress != $address) {
320
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
321
            }
322
323
            if ($checkRedeemScript != $redeemScript) {
324
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript}]");
325
            }
326
        } else {
327
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
328
        }
329
330
        return (string)$path;
331
    }
332
333
    /**
334
     * @param string|BIP32Path  $path
335
     * @return BIP32Key|false
336
     * @throws \Exception
337
     *
338
     * @TODO: hmm?
339
     */
340
    protected function getParentPublicKey($path) {
341
        $path = BIP32Path::path($path)->parent()->publicPath();
342
343
        if ($path->count() <= 2) {
344
            return false;
345
        }
346
347
        if ($path->isHardened()) {
348
            return false;
349
        }
350
351
        if (!isset($this->pubKeys[(string)$path])) {
352
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
353
        }
354
355
        return $this->pubKeys[(string)$path];
356
    }
357
358
    /**
359
     * get address for the specified path
360
     *
361
     * @param string|BIP32Path  $path
362
     * @return string
363
     */
364
    public function getAddressByPath($path) {
365
        $path = (string)BIP32Path::path($path)->privatePath();
366
        if (!isset($this->derivations[$path])) {
367
            list($address, ) = $this->getRedeemScriptByPath($path);
368
369
            $this->derivations[$path] = $address;
370
            $this->derivationsByAddress[$address] = $path;
371
        }
372
373
        return $this->derivations[$path];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->derivations[$path]; of type string[]|string adds the type string[] to the return on line 373 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getAddressByPath of type string.
Loading history...
374
    }
375
376
    /**
377
     * get address and redeemScript for specified path
378
     *
379
     * @param string    $path
380
     * @return array[string, string]     [address, redeemScript]
0 ignored issues
show
Documentation introduced by
The doc-type array[string, could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
381
     */
382
    public function getRedeemScriptByPath($path) {
383
        $path = BIP32Path::path($path);
384
385
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
386
        if ($pubKey = $this->getParentPublicKey($path)) {
387
            $key = $pubKey->buildKey($path->publicPath());
388
        } else {
389
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
390
        }
391
392
        return $this->getRedeemScriptFromKey($key, $path);
393
    }
394
395
    /**
396
     * @param BIP32Key          $key
397
     * @param string|BIP32Path  $path
398
     * @return string
399
     */
400
    protected function getAddressFromKey(BIP32Key $key, $path) {
401
        return $this->getRedeemScriptFromKey($key, $path)[0];
402
    }
403
404
    /**
405
     * @param BIP32Key          $key
406
     * @param string|BIP32Path  $path
407
     * @return string[]                 [address, redeemScript]
408
     * @throws \Exception
409
     */
410
    protected function getRedeemScriptFromKey(BIP32Key $key, $path) {
411
        $path = BIP32Path::path($path)->publicPath();
412
413
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
414
415
        $multiSig = RawTransaction::create_multisig(
416
            2,
417
            BlocktrailSDK::sortMultisigKeys([
0 ignored issues
show
Documentation introduced by
array($key->buildKey($pa...ey($path)->publicKey()) is of type array<integer,string|fal...e","2":"string|false"}>, but the function expects a array<integer,string>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
418
                $key->buildKey($path)->publicKey(),
419
                $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
420
                $blocktrailPublicKey->buildKey($path)->publicKey()
421
            ])
422
        );
423
424
        return [$multiSig['address'], $multiSig['redeemScript']];
425
    }
426
427
    /**
428
     * get the path (and redeemScript) to specified address
429
     *
430
     * @param string $address
431
     * @return array
432
     */
433
    public function getPathForAddress($address) {
434
        return $this->sdk->getPathForAddress($this->identifier, $address);
435
    }
436
437
    /**
438
     * @param string|BIP32Path  $path
439
     * @return BIP32Key
440
     * @throws \Exception
441
     */
442 View Code Duplication
    public function getBlocktrailPublicKey($path) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
443
        $path = BIP32Path::path($path);
444
445
        $keyIndex = str_replace("'", "", $path[1]);
446
447
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
448
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
449
        }
450
451
        return $this->blocktrailPublicKeys[$keyIndex];
452
    }
453
454
    /**
455
     * generate a new derived key and return the new path and address for it
456
     *
457
     * @return string[]     [path, address]
458
     */
459
    public function getNewAddressPair() {
460
        $path = $this->getNewDerivation();
461
        $address = $this->getAddressByPath($path);
462
463
        return [$path, $address];
0 ignored issues
show
Best Practice introduced by
The expression return array($path, $address); seems to be an array, but some of its elements' types (string[]) are incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddressPair of type string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
464
    }
465
466
    /**
467
     * generate a new derived private key and return the new address for it
468
     *
469
     * @return string
470
     */
471
    public function getNewAddress() {
472
        return $this->getNewAddressPair()[1];
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getNewAddressPair()[1]; of type string[]|string adds the type string[] to the return on line 472 which is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getNewAddress of type string.
Loading history...
473
    }
474
475
    /**
476
     * get the balance for the wallet
477
     *
478
     * @return int[]            [confirmed, unconfirmed]
479
     */
480
    public function getBalance() {
481
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
482
483
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
484
    }
485
486
    /**
487
     * do wallet discovery (slow)
488
     *
489
     * @param int   $gap        the gap setting to use for discovery
490
     * @return int[]            [confirmed, unconfirmed]
491
     */
492
    public function doDiscovery($gap = 200) {
493
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
494
495
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
496
    }
497
498
    /**
499
     * create, sign and send a transaction
500
     *
501
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
502
     *                                      value should be INT
503
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
504
     * @param bool     $allowZeroConf
505
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
506
     * @param string   $feeStrategy
507
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
508
     * @return string the txid / transaction hash
509
     * @throws \Exception
510
     */
511
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
512
        if ($this->locked) {
513
            throw new \Exception("Wallet needs to be unlocked to pay");
514
        }
515
516
        $outputs = self::normalizeOutputsStruct($outputs);
517
518
        $txBuilder = new TransactionBuilder();
519
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
520
        $txBuilder->setFeeStrategy($feeStrategy);
521
522
        foreach ($outputs as $output) {
523
            $txBuilder->addRecipient($output['address'], $output['value']);
524
        }
525
526
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
527
528
        $apiCheckFee = $forceFee === null;
529
530
        return $this->sendTx($txBuilder, $apiCheckFee);
531
    }
532
533
    /**
534
     * determine max spendable from wallet after fees
535
     *
536
     * @param bool     $allowZeroConf
537
     * @param string   $feeStrategy
538
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
539
     * @param int      $outputCnt
540
     * @return string
541
     * @throws BlocktrailSDKException
542
     */
543
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
544
        return $this->sdk->walletMaxSpendable($this->identifier, $allowZeroConf, $feeStrategy, $forceFee, $outputCnt);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->sdk->walle...$forceFee, $outputCnt); (array) is incompatible with the return type declared by the interface Blocktrail\SDK\WalletInterface::getMaxSpendable of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
545
    }
546
547
    /**
548
     * parse outputs into normalized struct
549
     *
550
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
551
     * @return array            [['address' => address, 'value' => value], ]
552
     */
553
    public static function normalizeOutputsStruct(array $outputs) {
554
        $result = [];
555
556
        foreach ($outputs as $k => $v) {
557
            if (is_numeric($k)) {
558
                if (!is_array($v)) {
559
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
560
                }
561
562
                if (isset($v['address']) && isset($v['value'])) {
563
                    $address = $v['address'];
564
                    $value = $v['value'];
565
                } else if (count($v) == 2 && isset($v[0]) && isset($v[1])) {
566
                    $address = $v[0];
567
                    $value = $v[1];
568
                } else {
569
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
570
                }
571
            } else {
572
                $address = $k;
573
                $value = $v;
574
            }
575
576
            $result[] = ['address' => $address, 'value' => $value];
577
        }
578
579
        return $result;
580
    }
581
582
    /**
583
     * 'fund' the txBuilder with UTXOs (modified in place)
584
     *
585
     * @param TransactionBuilder    $txBuilder
586
     * @param bool|true             $lockUTXOs
587
     * @param bool|false            $allowZeroConf
588
     * @param null|int              $forceFee
589
     * @return TransactionBuilder
590
     */
591
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
592
        // get the data we should use for this transaction
593
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
594
        $utxos = $coinSelection['utxos'];
595
        $fee = $coinSelection['fee'];
596
        $change = $coinSelection['change'];
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
597
598
        if ($forceFee !== null) {
599
            $txBuilder->setFee($forceFee);
600
        } else {
601
            $txBuilder->validateFee($fee);
602
        }
603
604
        foreach ($utxos as $utxo) {
605
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script']);
606
        }
607
608
        return $txBuilder;
609
    }
610
611
    /**
612
     * build inputs and outputs lists for TransactionBuilder
613
     *
614
     * @param TransactionBuilder $txBuilder
615
     * @return array
616
     * @throws \Exception
617
     */
618
    public function buildTx(TransactionBuilder $txBuilder) {
619
        $send = $txBuilder->getOutputs();
620
621
        $utxos = $txBuilder->getUtxos();
622
623
        foreach ($utxos as $utxo) {
624
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKeyHex) {
625
                $tx = $this->sdk->transaction($utxo->hash);
626
627
                if (!$tx || !isset($tx['outputs'][$utxo->index])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
628
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
629
                }
630
631
                $output = $tx['outputs'][$utxo->index];
632
633
                if (!$utxo->address) {
634
                    $utxo->address = $output['address'];
635
                }
636
                if (!$utxo->value) {
637
                    $utxo->value = $output['value'];
638
                }
639
                if (!$utxo->scriptPubKeyHex) {
640
                    $utxo->scriptPubKeyHex = $output['script_hex'];
641
                }
642
            }
643
644
            if (!$utxo->path) {
645
                $address = $utxo->address;
646
                if (!BitcoinLib::validate_address($address)) {
647
                    throw new \Exception("Invalid address [{$address}]");
648
                }
649
650
                $utxo->path = $this->getPathForAddress($address);
651
            }
652
653
            if (!$utxo->redeemScript) {
654
                list(, $redeemScript) = $this->getRedeemScriptByPath($utxo->path);
655
                $utxo->redeemScript = $redeemScript;
656
            }
657
        }
658
659
        if (array_sum(array_map(function (UTXO $utxo) { return $utxo->value; }, $utxos)) < array_sum(array_column($send, 'value'))) {
660
            throw new \Exception("Atempting to spend more than sum of UTXOs");
661
        }
662
663
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
664
665
        if ($txBuilder->getValidateFee() !== null) {
666
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
667
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
668
            }
669
        }
670
671
        if ($change > 0) {
672
            $send[] = [
673
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
674
                'value' => $change
675
            ];
676
        }
677
678
        // create raw transaction
679
        $inputs = array_map(function (UTXO $utxo) {
680
            return [
681
                'txid' => $utxo->hash,
682
                'vout' => (int)$utxo->index,
683
                'address' => $utxo->address,
684
                'scriptPubKey' => $utxo->scriptPubKeyHex,
685
                'value' => $utxo->value,
686
                'path' => $utxo->path,
687
                'redeemScript' => $utxo->redeemScript
688
            ];
689
        }, $utxos);
690
691
692
        // outputs should be randomized to make the change harder to detect
693
        if ($txBuilder->shouldRandomizeChangeOuput()) {
694
            shuffle($send);
695
        }
696
697
        return [$inputs, $send];
698
    }
699
700
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
701
        $send = $txBuilder->getOutputs();
702
        $utxos = $txBuilder->getUtxos();
703
704
        $fee = $txBuilder->getFee();
705
        $change = null;
0 ignored issues
show
Unused Code introduced by
$change is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
706
707
        // if the fee is fixed we just need to calculate the change
708
        if ($fee !== null) {
709
            $change = $this->determineChange($utxos, $send, $fee);
710
711
            // if change is not dust we need to add a change output
712 View Code Duplication
            if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
713
                $send[] = ['address' => 'change', 'value' => $change];
714
            } else {
715
                // if change is dust we do nothing (implicitly it's added to the fee)
716
                $change = 0;
717
            }
718
        } else {
719
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
720
721
            $change = $this->determineChange($utxos, $send, $fee);
722
723
            if ($change > 0) {
724
                $changeIdx = count($send);
725
                // set dummy change output
726
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
727
728
                // recaculate fee now that we know that we have a change output
729
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
730
731
                // unset dummy change output
732
                unset($send[$changeIdx]);
733
734
                // if adding the change output made the fee bump up and the change is smaller than the fee
735
                //  then we're not doing change
736
                if ($fee2 > $fee && $fee2 > $change) {
737
                    $change = 0;
738
                } else {
739
                    $change = $this->determineChange($utxos, $send, $fee2);
740
741
                    // if change is not dust we need to add a change output
742 View Code Duplication
                    if ($change > Blocktrail::DUST) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
743
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
744
                    } else {
745
                        // if change is dust we do nothing (implicitly it's added to the fee)
746
                        $change = 0;
747
                    }
748
                }
749
            }
750
        }
751
752
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
753
754
        return [$fee, $change];
755
    }
756
757
    /**
758
     * create, sign and send transction based on TransactionBuilder
759
     *
760
     * @param TransactionBuilder $txBuilder
761
     * @param bool $apiCheckFee     let the API check if the fee is correct
762
     * @return string
763
     * @throws \Exception
764
     */
765
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
766
        list($inputs, $outputs) = $this->buildTx($txBuilder);
767
768
        return $this->_sendTx($inputs, $outputs, $apiCheckFee);
769
    }
770
771
    /**
772
     * !! INTERNAL METHOD, public for testing purposes !!
773
     * create, sign and send transction based on inputs and outputs
774
     *
775
     * @param      $inputs
776
     * @param      $outputs
777
     * @param bool $apiCheckFee     let the API check if the fee is correct
778
     * @return string
779
     * @throws \Exception
780
     * @internal
781
     */
782
    public function _sendTx($inputs, $outputs, $apiCheckFee = true) {
783
        if ($this->locked) {
784
            throw new \Exception("Wallet needs to be unlocked to pay");
785
        }
786
787
        // create raw unsigned TX
788
        $raw_transaction = RawTransaction::create($inputs, $outputs);
789
790
        if (!$raw_transaction) {
791
            throw new \Exception("Failed to create raw transaction");
792
        }
793
794
        // sign the transaction with our keys
795
        $signed = $this->signTransaction($raw_transaction, $inputs);
796
797
        if (!$signed['sign_count']) {
798
            throw new \Exception("Failed to partially sign transaction");
799
        }
800
801
        // send the transaction
802
        $finished = $this->sendTransaction($signed['hex'], array_column($inputs, 'path'), $apiCheckFee);
803
804
        return $finished;
805
    }
806
807
    /**
808
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
809
     *
810
     * @param int $utxoCnt      number of unspent inputs in transaction
811
     * @param int $outputCnt    number of outputs in transaction
812
     * @return float
813
     * @access public           reminder that people might use this!
814
     */
815
    public static function estimateFee($utxoCnt, $outputCnt) {
816
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
817
818
        return self::baseFeeForSize($size);
819
    }
820
821
    /**
822
     * @param int $size     size in bytes
823
     * @return int          fee in satoshi
824
     */
825
    public static function baseFeeForSize($size) {
826
        $sizeKB = (int)ceil($size / 1000);
827
828
        return $sizeKB * self::BASE_FEE;
829
    }
830
831
    /**
832
     * @param int $txinSize
833
     * @param int $txoutSize
834
     * @return float
835
     */
836
    public static function estimateSize($txinSize, $txoutSize) {
837
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
838
    }
839
840
    /**
841
     * only supports estimating size for P2PKH/P2SH outputs
842
     *
843
     * @param int $outputCnt    number of outputs in transaction
844
     * @return float
845
     */
846
    public static function estimateSizeOutputs($outputCnt) {
847
        return ($outputCnt * 34);
848
    }
849
850
    /**
851
     * only supports estimating size for 2of3 multsig UTXOs
852
     *
853
     * @param int $utxoCnt      number of unspent inputs in transaction
854
     * @return float
855
     */
856
    public static function estimateSizeUTXOs($utxoCnt) {
857
        $txinSize = 0;
858
859
        for ($i=0; $i<$utxoCnt; $i++) {
860
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
861
            $multisig = "2of3";
862
863
            if ($multisig) {
864
                $sigCnt = 2;
865
                $msig = explode("of", $multisig);
866
                if (count($msig) == 2 && is_numeric($msig[0])) {
867
                    $sigCnt = $msig[0];
868
                }
869
870
                $txinSize += array_sum([
871
                    32, // txhash
872
                    4, // idx
873
                    3, // scriptVarInt[>=253]
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
874
                    ((1 + 72) * $sigCnt), // (OP_PUSHDATA[<75] + 72) * sigCnt
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
875
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
876
                    4, // sequence
877
                ]);
878
            } else {
879
                $txinSize += array_sum([
880
                    32, // txhash
881
                    4, // idx
882
                    73, // sig
883
                    34, // script
884
                    4, // sequence
885
                ]);
886
            }
887
        }
888
889
        return $txinSize;
890
    }
891
892
    /**
893
     * determine how much fee is required based on the inputs and outputs
894
     *  this is an estimation, not a proper 100% correct calculation
895
     *
896
     * @param UTXO[]  $utxos
897
     * @param array[] $outputs
898
     * @param         $feeStrategy
899
     * @param         $optimalFeePerKB
900
     * @param         $lowPriorityFeePerKB
901
     * @return int
902
     * @throws BlocktrailSDKException
903
     */
904
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
905
        $outputSize = 0;
906
        foreach ($outputs as $output) {
907
            if (isset($output['scriptPubKey'])) {
908
                $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
909
            } else {
910
                $outputSize += 34;
911
            }
912
        }
913
914
        $size = self::estimateSize(self::estimateSizeUTXOs(count($utxos)), $outputSize);
915
916
        switch ($feeStrategy) {
917
            case self::FEE_STRATEGY_BASE_FEE:
918
                return self::baseFeeForSize($size);
919
920
            case self::FEE_STRATEGY_OPTIMAL:
921
                return (int)round(($size / 1000) * $optimalFeePerKB);
922
923
            case self::FEE_STRATEGY_LOW_PRIORITY:
924
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
925
926
            default:
927
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
928
        }
929
    }
930
931
    /**
932
     * determine how much change is left over based on the inputs and outputs and the fee
933
     *
934
     * @param UTXO[]    $utxos
935
     * @param array[]   $outputs
936
     * @param int       $fee
937
     * @return int
938
     */
939
    protected function determineChange($utxos, $outputs, $fee) {
940
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
941
            return $utxo->value;
942
        }, $utxos));
943
        $outputsTotal = array_sum(array_column($outputs, 'value'));
944
945
        return $inputsTotal - $outputsTotal - $fee;
946
    }
947
948
    /**
949
     * sign a raw transaction with the private keys that we have
950
     *
951
     * @param string    $raw_transaction
952
     * @param array[]   $inputs
953
     * @return array                        response from RawTransaction::sign
954
     * @throws \Exception
955
     */
956
    protected function signTransaction($raw_transaction, array $inputs) {
957
        $wallet = [];
958
        $keys = [];
959
        $redeemScripts = [];
960
961
        foreach ($inputs as $input) {
962
            $redeemScript = null;
0 ignored issues
show
Unused Code introduced by
$redeemScript is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
963
            $key = null;
0 ignored issues
show
Unused Code introduced by
$key is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
964
965
            if (isset($input['redeemScript'], $input['path'])) {
966
                $redeemScript = $input['redeemScript'];
967
                $path = BIP32Path::path($input['path'])->privatePath();
968
                $key = $this->primaryPrivateKey->buildKey($path);
969
                $address = $this->getAddressFromKey($key, $path);
970
971
                if ($address != $input['address']) {
972
                    throw new \Exception("Generated address does not match expected address!");
973
                }
974
            } else {
975
                throw new \Exception("No redeemScript/path for input");
976
            }
977
978
            if ($redeemScript && $key) {
979
                $keys[] = $key;
980
                $redeemScripts[] = $redeemScript;
981
            }
982
        }
983
984
        BIP32::bip32_keys_to_wallet($wallet, array_map(function (BIP32Key $key) {
985
            return $key->tuple();
986
        }, $keys));
987
        RawTransaction::redeem_scripts_to_wallet($wallet, $redeemScripts);
988
989
        return RawTransaction::sign($wallet, $raw_transaction, json_encode($inputs));
990
    }
991
992
    /**
993
     * send the transaction using the API
994
     *
995
     * @param string    $signed
996
     * @param string[]  $paths
997
     * @param bool      $checkFee
998
     * @return string           the complete raw transaction
999
     * @throws \Exception
1000
     */
1001
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1002
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1003
    }
1004
1005
    /**
1006
     * use the API to get the best inputs to use based on the outputs
1007
     *
1008
     * @param array[]  $outputs
1009
     * @param bool     $lockUTXO
1010
     * @param bool     $allowZeroConf
1011
     * @param string   $feeStrategy
1012
     * @param null|int $forceFee
1013
     * @return array
1014
     */
1015
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1016
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1017
1018
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1019
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1020
        $this->feePerKBAge = time();
1021
1022
        return $result;
1023
    }
1024
1025
    public function getOptimalFeePerKB() {
1026
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1027
            $this->updateFeePerKB();
1028
        }
1029
1030
        return $this->optimalFeePerKB;
1031
    }
1032
1033
    public function getLowPriorityFeePerKB() {
1034
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1035
            $this->updateFeePerKB();
1036
        }
1037
1038
        return $this->lowPriorityFeePerKB;
1039
    }
1040
1041
    public function updateFeePerKB() {
1042
        $result = $this->sdk->feePerKB();
1043
1044
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1045
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1046
1047
        $this->feePerKBAge = time();
1048
    }
1049
1050
    /**
1051
     * delete the wallet
1052
     *
1053
     * @param bool $force ignore warnings (such as non-zero balance)
1054
     * @return mixed
1055
     * @throws \Exception
1056
     */
1057
    public function deleteWallet($force = false) {
1058
        if ($this->locked) {
1059
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1060
        }
1061
1062
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1063
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1064
    }
1065
1066
    /**
1067
     * create checksum to verify ownership of the master primary key
1068
     *
1069
     * @return string[]     [address, signature]
1070
     */
1071
    protected function createChecksumVerificationSignature() {
1072
        $import = BIP32::import($this->primaryPrivateKey->key());
1073
1074
        $public = $this->primaryPrivateKey->publicKey();
1075
        $address = BitcoinLib::public_key_to_address($public, $import['version']);
0 ignored issues
show
Security Bug introduced by
It seems like $public defined by $this->primaryPrivateKey->publicKey() on line 1074 can also be of type false; however, BitWasp\BitcoinLib\Bitco...public_key_to_address() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1076
1077
        return [$address, BitcoinLib::signMessage($address, $import)];
1078
    }
1079
1080
    /**
1081
     * setup a webhook for our wallet
1082
     *
1083
     * @param string    $url            URL to receive webhook events
1084
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1085
     * @return array
1086
     */
1087
    public function setupWebhook($url, $identifier = null) {
1088
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1089
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1090
    }
1091
1092
    /**
1093
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1094
     * @return mixed
1095
     */
1096
    public function deleteWebhook($identifier = null) {
1097
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1098
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1099
    }
1100
1101
    /**
1102
     * lock a specific unspent output
1103
     *
1104
     * @param     $txHash
1105
     * @param     $txIdx
1106
     * @param int $ttl
1107
     * @return bool
1108
     */
1109
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1110
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1111
    }
1112
1113
    /**
1114
     * unlock a specific unspent output
1115
     *
1116
     * @param     $txHash
1117
     * @param     $txIdx
1118
     * @return bool
1119
     */
1120
    public function unlockUTXO($txHash, $txIdx) {
1121
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1122
    }
1123
1124
    /**
1125
     * get all transactions for the wallet (paginated)
1126
     *
1127
     * @param  integer $page    pagination: page number
1128
     * @param  integer $limit   pagination: records per page (max 500)
1129
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1130
     * @return array            associative array containing the response
1131
     */
1132
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1133
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1134
    }
1135
1136
    /**
1137
     * get all addresses for the wallet (paginated)
1138
     *
1139
     * @param  integer $page    pagination: page number
1140
     * @param  integer $limit   pagination: records per page (max 500)
1141
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1142
     * @return array            associative array containing the response
1143
     */
1144
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1145
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1146
    }
1147
1148
    /**
1149
     * get all UTXOs for the wallet (paginated)
1150
     *
1151
     * @param  integer $page    pagination: page number
1152
     * @param  integer $limit   pagination: records per page (max 500)
1153
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1154
     * @return array            associative array containing the response
1155
     */
1156
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc') {
1157
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir);
1158
    }
1159
}
1160