Completed
Pull Request — master (#87)
by thomas
20:26
created

Wallet::__construct()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 33
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.1158

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 4
nop 10
dl 0
loc 33
ccs 20
cts 24
cp 0.8333
crap 5.1158
rs 8.439
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\Buffertools\Buffer;
21
use Blocktrail\SDK\Bitcoin\BIP32Key;
22
use Blocktrail\SDK\Bitcoin\BIP32Path;
23
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
24
25
/**
26
 * Class Wallet
27
 */
28
abstract class Wallet implements WalletInterface {
29
30
    const WALLET_VERSION_V1 = 'v1';
31
    const WALLET_VERSION_V2 = 'v2';
32
    const WALLET_VERSION_V3 = 'v3';
33
34
    const CHAIN_BTC_DEFAULT = 0;
35
    const CHAIN_BCC_DEFAULT = 1;
36
    const CHAIN_BTC_SEGWIT = 2;
37
38
    const BASE_FEE = 10000;
39
40
    /**
41
     * development / debug setting
42
     *  when getting a new derivation from the API,
43
     *  will verify address / redeeemScript with the values the API provides
44
     */
45
    const VERIFY_NEW_DERIVATION = true;
46
47
    /**
48
     * @var BlocktrailSDKInterface
49
     */
50
    protected $sdk;
51
52
    /**
53
     * @var string
54
     */
55
    protected $identifier;
56
57
    /**
58
     * BIP32 master primary private key (m/)
59
     *
60
     * @var BIP32Key
61
     */
62
    protected $primaryPrivateKey;
63
64
    /**
65
     * @var BIP32Key[]
66
     */
67
    protected $primaryPublicKeys;
68
69
    /**
70
     * BIP32 master backup public key (M/)
71
72
     * @var BIP32Key
73
     */
74
    protected $backupPublicKey;
75
76
    /**
77
     * map of blocktrail BIP32 public keys
78
     *  keyed by key index
79
     *  path should be `M / key_index'`
80
     *
81
     * @var BIP32Key[]
82
     */
83
    protected $blocktrailPublicKeys;
84
85
    /**
86
     * the 'Blocktrail Key Index' that is used for new addresses
87
     *
88
     * @var int
89
     */
90
    protected $keyIndex;
91
92
    /**
93
     * 'bitcoin'
94
     *
95
     * @var string
96
     */
97
    protected $network;
98
99
    /**
100
     * testnet yes / no
101
     *
102
     * @var bool
103
     */
104
    protected $testnet;
105
106
    /**
107
     * cache of public keys, by path
108
     *
109
     * @var BIP32Key[]
110
     */
111
    protected $pubKeys = [];
112
113
    /**
114
     * cache of address / redeemScript, by path
115
     *
116
     * @var string[][]      [[address, redeemScript)], ]
117
     */
118
    protected $derivations = [];
119
120
    /**
121
     * reverse cache of paths by address
122
     *
123
     * @var string[]
124
     */
125
    protected $derivationsByAddress = [];
126
127
    /**
128
     * @var WalletPath
129
     */
130
    protected $walletPath;
131
132
    protected $checksum;
133
134
    protected $locked = true;
135
    protected $isSegwit = false;
136
    protected $chainIndex = false;
137
    protected $changeIndex = false;
138
139
    protected $optimalFeePerKB;
140
    protected $lowPriorityFeePerKB;
141
    protected $feePerKBAge;
142
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
143
144
    /**
145
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
146
     * @param string                        $identifier                 identifier of the wallet
147
     * @param BIP32Key[]                    $primaryPublicKeys
148
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
149
     * @param BIP32Key[]                    $blocktrailPublicKeys
150
     * @param int                           $keyIndex
151
     * @param string                        $network
152
     * @param bool                          $testnet
153
     * @param bool                          $segwit
154
     * @param string                        $checksum
155
     * @throws BlocktrailSDKException
156
     */
157 21
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, $checksum) {
158 21
        $this->sdk = $sdk;
159
160 21
        $this->identifier = $identifier;
161 21
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
162 21
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
163 21
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
164
165 21
        $this->network = $network;
166 21
        $this->testnet = $testnet;
167 21
        $this->keyIndex = $keyIndex;
168 21
        $this->checksum = $checksum;
169
170 21
        if ($network === "bitcoin") {
171 21
            if ($segwit) {
172 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
173 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
174
            } else {
175 19
                $chainIdx = self::CHAIN_BTC_DEFAULT;
176 21
                $changeIdx = self::CHAIN_BTC_DEFAULT;
177
            }
178
        } else {
179
            if ($segwit && $network === "bitcoincash") {
180
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
181
            }
182
            $chainIdx = self::CHAIN_BCC_DEFAULT;
183
            $changeIdx = self::CHAIN_BCC_DEFAULT;
184
        }
185
186 21
        $this->isSegwit = (bool) $segwit;
187 21
        $this->chainIndex = $chainIdx;
0 ignored issues
show
Documentation Bug introduced by
The property $chainIndex was declared of type boolean, but $chainIdx is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
188 21
        $this->changeIndex = $changeIdx;
0 ignored issues
show
Documentation Bug introduced by
The property $changeIndex was declared of type boolean, but $changeIdx is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
189 21
    }
190
191
    /**
192
     * @param null $chainIndex
193
     * @return BIP32Path
194
     */
195 13
    protected function getWalletPath($chainIndex = null) {
196 13
        if ($chainIndex === null) {
197 12
            return WalletPath::create($this->keyIndex, $this->chainIndex)->path()->last("*");
0 ignored issues
show
Documentation introduced by
$this->chainIndex is of type boolean, but the function expects a integer.

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...
198
        } else {
199 4
            return WalletPath::create($this->keyIndex, $chainIndex)->path()->last("*");
200
        }
201
    }
202
203
    /**
204
     * @return bool
205
     */
206 3
    public function isSegwit() {
207 3
        return $this->isSegwit;
208
    }
209
210
    /**
211
     * return the wallet identifier
212
     *
213
     * @return string
214
     */
215 10
    public function getIdentifier() {
216 10
        return $this->identifier;
217
    }
218
219
    /**
220
     * Returns the wallets backup public key
221
     *
222
     * @return [xpub, path]
0 ignored issues
show
Documentation introduced by
The doc-type xpub,">[xpub, could not be parsed: Unknown type name "[" at position 0. [(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...
223
     */
224 1
    public function getBackupKey() {
225 1
        return $this->backupPublicKey->tuple();
226
    }
227
228
    /**
229
     * return list of Blocktrail co-sign extended public keys
230
     *
231
     * @return array[]      [ [xpub, path] ]
232
     */
233 5
    public function getBlocktrailPublicKeys() {
234
        return array_map(function (BIP32Key $key) {
235 5
            return $key->tuple();
236 5
        }, $this->blocktrailPublicKeys);
237
    }
238
239
    /**
240
     * check if wallet is locked
241
     *
242
     * @return bool
243
     */
244 10
    public function isLocked() {
245 10
        return $this->locked;
246
    }
247
248
    /**
249
     * upgrade wallet to different blocktrail cosign key
250
     *
251
     * @param $keyIndex
252
     * @return bool
253
     * @throws \Exception
254
     */
255 5
    public function upgradeKeyIndex($keyIndex) {
256 5
        if ($this->locked) {
257 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
258
        }
259
260 5
        $walletPath = WalletPath::create($keyIndex);
261
262
        // do the upgrade to the new 'key_index'
263 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
264
265
        // $primaryPublicKey = BIP32::extended_private_to_public(BIP32::build_key($this->primaryPrivateKey->tuple(), (string)$walletPath->keyIndexPath()));
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
266 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
267
268 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
269
270 5
        $this->keyIndex = $keyIndex;
271
272
        // update the blocktrail public keys
273 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
274 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
275 5
                $path = $pubKey[1];
276 5
                $pubKey = $pubKey[0];
277 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
278
            }
279
        }
280
281 5
        return true;
282
    }
283
284
    /**
285
     * get a new BIP32 derivation for the next (unused) address
286
     *  by requesting it from the API
287
     *
288
     * @return string
289
     * @param int|null $chainIndex
290
     * @throws \Exception
291
     */
292 13
    protected function getNewDerivation($chainIndex = null) {
293 13
        $path = $this->getWalletPath($chainIndex);
0 ignored issues
show
Bug introduced by
It seems like $chainIndex defined by parameter $chainIndex on line 292 can also be of type integer; however, Blocktrail\SDK\Wallet::getWalletPath() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
294
295 13
        if (self::VERIFY_NEW_DERIVATION) {
296 13
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
297
298 13
            $path = $new['path'];
299 13
            $address = $new['address'];
300 13
            $redeemScript = $new['redeem_script'];
301 13
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
302
303
            /** @var ScriptInterface $checkRedeemScript */
304
            /** @var ScriptInterface $checkWitnessScript */
305 13
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
306
307 13
            if ($checkAddress != $address) {
308
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
309
            }
310
311 13
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
312
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
313
            }
314
315 13
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
316 13
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
317
            }
318
        } else {
319
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
320
        }
321
322 13
        return (string)$path;
323
    }
324
325
    /**
326
     * @param string|BIP32Path  $path
327
     * @return BIP32Key|false
328
     * @throws \Exception
329
     *
330
     * @TODO: hmm?
331
     */
332 16
    protected function getParentPublicKey($path) {
333 16
        $path = BIP32Path::path($path)->parent()->publicPath();
334
335 16
        if ($path->count() <= 2) {
336
            return false;
337
        }
338
339 16
        if ($path->isHardened()) {
340
            return false;
341
        }
342
343 16
        if (!isset($this->pubKeys[(string)$path])) {
344 16
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
345
        }
346
347 16
        return $this->pubKeys[(string)$path];
348
    }
349
350
    /**
351
     * get address for the specified path
352
     *
353
     * @param string|BIP32Path  $path
354
     * @return string
355
     */
356 13
    public function getAddressByPath($path) {
357 13
        $path = (string)BIP32Path::path($path)->privatePath();
358 13
        if (!isset($this->derivations[$path])) {
359 13
            list($address, ) = $this->getRedeemScriptByPath($path);
360
361 13
            $this->derivations[$path] = $address;
362 13
            $this->derivationsByAddress[$address] = $path;
363
        }
364
365 13
        return $this->derivations[$path];
366
    }
367
368
    /**
369
     * @param string $path
370
     * @return WalletScript
371
     */
372 16
    public function getWalletScriptByPath($path) {
373 16
        $path = BIP32Path::path($path);
374
375
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
376 16
        if ($pubKey = $this->getParentPublicKey($path)) {
377 16
            $key = $pubKey->buildKey($path->publicPath());
378
        } else {
379
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
380
        }
381
382 16
        return $this->getWalletScriptFromKey($key, $path);
383
    }
384
385
    /**
386
     * get address and redeemScript for specified path
387
     *
388
     * @param string    $path
389
     * @return array[string, ScriptInterface, ScriptInterface|null]     [address, redeemScript, witnessScript]
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...
390
     */
391 15
    public function getRedeemScriptByPath($path) {
392 15
        $walletScript = $this->getWalletScriptByPath($path);
393
394 15
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
395 15
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
396 15
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
397
    }
398
399
    /**
400
     * @param BIP32Key          $key
401
     * @param string|BIP32Path  $path
402
     * @return string
403
     */
404
    protected function getAddressFromKey(BIP32Key $key, $path) {
405
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
406
    }
407
408
    /**
409
     * @param BIP32Key          $key
410
     * @param string|BIP32Path  $path
411
     * @return WalletScript
412
     * @throws \Exception
413
     */
414 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
415 16
        $path = BIP32Path::path($path)->publicPath();
416
417 16
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
418
419 16
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
420 16
            $key->buildKey($path)->publicKey(),
421 16
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
422 16
            $blocktrailPublicKey->buildKey($path)->publicKey()
423 16
        ]), false);
424
425 16
        $type = (int)$key->path()[2];
426
427 16
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
428 3
            $witnessScript = new WitnessScript($multisig);
429 3
            $redeemScript = new P2shScript($witnessScript);
430 3
            $scriptPubKey = $redeemScript->getOutputScript();
431 16
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
432 15
            $witnessScript = null;
433 15
            $redeemScript = new P2shScript($multisig);
434 15
            $scriptPubKey = $redeemScript->getOutputScript();
435
        } else {
436 1
            throw new BlocktrailSDKException("Unsupported chain in path");
437
        }
438
439 15
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
440
    }
441
442
    /**
443
     * get the path (and redeemScript) to specified address
444
     *
445
     * @param string $address
446
     * @return array
447
     */
448 1
    public function getPathForAddress($address) {
449 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
450
    }
451
452
    /**
453
     * @param string|BIP32Path  $path
454
     * @return BIP32Key
455
     * @throws \Exception
456
     */
457 16
    public function getBlocktrailPublicKey($path) {
458 16
        $path = BIP32Path::path($path);
459
460 16
        $keyIndex = str_replace("'", "", $path[1]);
461
462 16
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
463
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
464
        }
465
466 16
        return $this->blocktrailPublicKeys[$keyIndex];
467
    }
468
469
    /**
470
     * generate a new derived key and return the new path and address for it
471
     *
472
     * @param int|null $chainIndex
473
     * @return string[]     [path, address]
474
     */
475 13
    public function getNewAddressPair($chainIndex = null) {
476 13
        $path = $this->getNewDerivation($chainIndex);
477 13
        $address = $this->getAddressByPath($path);
478
479 13
        return [$path, $address];
480
    }
481
482
    /**
483
     * generate a new derived private key and return the new address for it
484
     *
485
     * @param int|null $chainIndex
486
     * @return string
487
     */
488 8
    public function getNewAddress($chainIndex = null) {
489 8
        return $this->getNewAddressPair($chainIndex)[1];
490
    }
491
492
    /**
493
     * get the balance for the wallet
494
     *
495
     * @return int[]            [confirmed, unconfirmed]
496
     */
497 9
    public function getBalance() {
498 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
499
500 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
501
    }
502
503
    /**
504
     * do wallet discovery (slow)
505
     *
506
     * @param int   $gap        the gap setting to use for discovery
507
     * @return int[]            [confirmed, unconfirmed]
508
     */
509 2
    public function doDiscovery($gap = 200) {
510 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
511
512 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
513
    }
514
515
    /**
516
     * create, sign and send a transaction
517
     *
518
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
519
     *                                      value should be INT
520
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
521
     * @param bool     $allowZeroConf
522
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
523
     * @param string   $feeStrategy
524
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
525
     * @return string the txid / transaction hash
526
     * @throws \Exception
527
     */
528 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
529 9
        if ($this->locked) {
530 4
            throw new \Exception("Wallet needs to be unlocked to pay");
531
        }
532
533 9
        $outputs = self::normalizeOutputsStruct($outputs);
534
535 9
        $txBuilder = new TransactionBuilder();
536 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
537 9
        $txBuilder->setFeeStrategy($feeStrategy);
538 9
        $txBuilder->setChangeAddress($changeAddress);
539
540 9
        foreach ($outputs as $output) {
541 9
            $txBuilder->addRecipient($output['address'], $output['value']);
542
        }
543
544 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
545
546 3
        $apiCheckFee = $forceFee === null;
547
548 3
        return $this->sendTx($txBuilder, $apiCheckFee);
549
    }
550
551
    /**
552
     * determine max spendable from wallet after fees
553
     *
554
     * @param bool     $allowZeroConf
555
     * @param string   $feeStrategy
556
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
557
     * @param int      $outputCnt
558
     * @return string
559
     * @throws BlocktrailSDKException
560
     */
561
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
562
        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...
563
    }
564
565
    /**
566
     * parse outputs into normalized struct
567
     *
568
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
569
     * @return array            [['address' => address, 'value' => value], ]
570
     */
571 10
    public static function normalizeOutputsStruct(array $outputs) {
572 10
        $result = [];
573
574 10
        foreach ($outputs as $k => $v) {
575 10
            if (is_numeric($k)) {
576 1
                if (!is_array($v)) {
577
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
578
                }
579
580 1
                if (isset($v['address']) && isset($v['value'])) {
581 1
                    $address = $v['address'];
582 1
                    $value = $v['value'];
583 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
584 1
                    $address = $v[0];
585 1
                    $value = $v[1];
586
                } else {
587 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
588
                }
589
            } else {
590 10
                $address = $k;
591 10
                $value = $v;
592
            }
593
594 10
            $result[] = ['address' => $address, 'value' => $value];
595
        }
596
597 10
        return $result;
598
    }
599
600
    /**
601
     * 'fund' the txBuilder with UTXOs (modified in place)
602
     *
603
     * @param TransactionBuilder    $txBuilder
604
     * @param bool|true             $lockUTXOs
605
     * @param bool|false            $allowZeroConf
606
     * @param null|int              $forceFee
607
     * @return TransactionBuilder
608
     */
609 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
610
        // get the data we should use for this transaction
611 11
        $coinSelection = $this->coinSelection($txBuilder->getOutputs(/* $json = */true), $lockUTXOs, $allowZeroConf, $txBuilder->getFeeStrategy(), $forceFee);
0 ignored issues
show
Bug introduced by
It seems like $forceFee defined by parameter $forceFee on line 609 can also be of type integer; however, Blocktrail\SDK\Wallet::coinSelection() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
612
        
613 5
        $utxos = $coinSelection['utxos'];
614 5
        $fee = $coinSelection['fee'];
615 5
        $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...
616
617 5
        if ($forceFee !== null) {
618
            $txBuilder->setFee($forceFee);
619
        } else {
620 5
            $txBuilder->validateFee($fee);
621
        }
622
623 5
        foreach ($utxos as $utxo) {
624 5
            $signMode = SignInfo::MODE_SIGN;
625 5
            if (isset($utxo['sign_mode'])) {
626
                $signMode = $utxo['sign_mode'];
627
                if (!in_array($signMode, $this->allowedSignModes)) {
628
                    throw new \Exception("Sign mode disallowed by wallet");
629
                }
630
            }
631
632 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
633
        }
634
635 5
        return $txBuilder;
636
    }
637
638
    /**
639
     * build inputs and outputs lists for TransactionBuilder
640
     *
641
     * @param TransactionBuilder $txBuilder
642
     * @return [TransactionInterface, SignInfo[]]
0 ignored issues
show
Documentation introduced by
The doc-type TransactionInterface,">[TransactionInterface, could not be parsed: Unknown type name "[" at position 0. [(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...
643
     * @throws \Exception
644
     */
645 7
    public function buildTx(TransactionBuilder $txBuilder) {
646 7
        $send = $txBuilder->getOutputs();
647 7
        $utxos = $txBuilder->getUtxos();
648 7
        $signInfo = [];
649
650 7
        $txb = new TxBuilder();
651
652 7
        foreach ($utxos as $utxo) {
653 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
654 1
                $tx = $this->sdk->transaction($utxo->hash);
655
656 1
                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...
657
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
658
                }
659
660 1
                $output = $tx['outputs'][$utxo->index];
661
662 1
                if (!$utxo->address) {
663
                    $utxo->address = AddressFactory::fromString($output['address']);
664
                }
665 1
                if (!$utxo->value) {
666
                    $utxo->value = $output['value'];
667
                }
668 1
                if (!$utxo->scriptPubKey) {
669 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
670
                }
671
            }
672
673 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
674 7
                if (!$utxo->path) {
675
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
676
                }
677
678 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
679 6
                    list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
0 ignored issues
show
Documentation introduced by
$utxo->path is of type array, 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...
680 6
                    $utxo->redeemScript = $redeemScript;
681 6
                    $utxo->witnessScript = $witnessScript;
682
                }
683
            }
684
685 7
            $signInfo[] = $utxo->getSignInfo();
686
        }
687
688
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
689 7
            return $utxo->value;
690 7
        }, $utxos));
691 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
692 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
693
        }
694
695 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
696
697 7
        if ($txBuilder->getValidateFee() !== null) {
698 5
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
699
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
700
            }
701
        }
702
703 7
        if ($change > 0) {
704 4
            $send[] = [
705 4
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress($this->changeIndex),
0 ignored issues
show
Documentation introduced by
$this->changeIndex is of type boolean, but the function expects a integer|null.

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...
706 4
                'value' => $change
707
            ];
708
        }
709
710 7
        foreach ($utxos as $utxo) {
711 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
712
        }
713
714
        // outputs should be randomized to make the change harder to detect
715 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
716 7
            shuffle($send);
717
        }
718
719 7
        foreach ($send as $out) {
720 7
            assert(isset($out['value']));
721
722 7
            if (isset($out['scriptPubKey'])) {
723 1
                $txb->output($out['value'], $out['scriptPubKey']);
724 6
            } elseif (isset($out['address'])) {
725 6
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
726
            } else {
727 7
                throw new \Exception();
728
            }
729
        }
730
731 7
        return [$txb->get(), $signInfo];
732
    }
733
734 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
735 7
        $send = $txBuilder->getOutputs();
736 7
        $utxos = $txBuilder->getUtxos();
737
738 7
        $fee = $txBuilder->getFee();
739 7
        $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...
740
741
        // if the fee is fixed we just need to calculate the change
742 7
        if ($fee !== null) {
743 1
            $change = $this->determineChange($utxos, $send, $fee);
744
745
            // if change is not dust we need to add a change output
746 1
            if ($change > Blocktrail::DUST) {
747
                $send[] = ['address' => 'change', 'value' => $change];
748
            } else {
749
                // if change is dust we do nothing (implicitly it's added to the fee)
750 1
                $change = 0;
751
            }
752
        } else {
753 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
754
755 7
            $change = $this->determineChange($utxos, $send, $fee);
756
757 7
            if ($change > 0) {
758 6
                $changeIdx = count($send);
759
                // set dummy change output
760 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
761
762
                // recaculate fee now that we know that we have a change output
763 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
764
765
                // unset dummy change output
766 6
                unset($send[$changeIdx]);
767
768
                // if adding the change output made the fee bump up and the change is smaller than the fee
769
                //  then we're not doing change
770 6
                if ($fee2 > $fee && $fee2 > $change) {
771 2
                    $change = 0;
772
                } else {
773 5
                    $change = $this->determineChange($utxos, $send, $fee2);
774
775
                    // if change is not dust we need to add a change output
776 5
                    if ($change > Blocktrail::DUST) {
777 4
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
778
                    } else {
779
                        // if change is dust we do nothing (implicitly it's added to the fee)
780 1
                        $change = 0;
781
                    }
782
                }
783
            }
784
        }
785
786 7
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
787
788 7
        return [$fee, $change];
789
    }
790
791
    /**
792
     * create, sign and send transction based on TransactionBuilder
793
     *
794
     * @param TransactionBuilder $txBuilder
795
     * @param bool $apiCheckFee     let the API check if the fee is correct
796
     * @return string
797
     * @throws \Exception
798
     */
799 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
800 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
801
802 4
        return $this->_sendTx($tx, $signInfo, $apiCheckFee);
0 ignored issues
show
Compatibility introduced by
$tx of type object<BitWasp\Bitcoin\T...n\TransactionInterface> is not a sub-type of object<BitWasp\Bitcoin\Transaction\Transaction>. It seems like you assume a concrete implementation of the interface BitWasp\Bitcoin\Transaction\TransactionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
803
    }
804
805
    /**
806
     * !! INTERNAL METHOD, public for testing purposes !!
807
     * create, sign and send transction based on inputs and outputs
808
     *
809
     * @param Transaction $tx
810
     * @param SignInfo[]  $signInfo
811
     * @param bool $apiCheckFee     let the API check if the fee is correct
812
     * @return string
813
     * @throws \Exception
814
     * @internal
815
     */
816 4
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
817 4
        if ($this->locked) {
818
            throw new \Exception("Wallet needs to be unlocked to pay");
819
        }
820
821
        assert(Util::all(function ($signInfo) {
822 4
            return $signInfo instanceof SignInfo;
823 4
        }, $signInfo), '$signInfo should be SignInfo[]');
824
825
        // sign the transaction with our keys
826 4
        $signed = $this->signTransaction($tx, $signInfo);
827
828
        $txs = [
829 4
            'signed_transaction' => $signed->getHex(),
830 4
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
831
        ];
832
833
        // send the transaction
834
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
835 4
            return (string)$r->path;
836 4
        }, $signInfo), $apiCheckFee);
837
    }
838
839
    /**
840
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
841
     *
842
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
843
     * @param int $utxoCnt      number of unspent inputs in transaction
844
     * @param int $outputCnt    number of outputs in transaction
845
     * @return float
846
     * @access public           reminder that people might use this!
847
     */
848 1
    public static function estimateFee($utxoCnt, $outputCnt) {
849 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
850
851 1
        return self::baseFeeForSize($size);
852
    }
853
854
    /**
855
     * @param int $size     size in bytes
856
     * @return int          fee in satoshi
857
     */
858 5
    public static function baseFeeForSize($size) {
859 5
        $sizeKB = (int)ceil($size / 1000);
860
861 5
        return $sizeKB * self::BASE_FEE;
862
    }
863
864
    /**
865
     * @todo: variable varint
866
     * @param int $txinSize
867
     * @param int $txoutSize
868
     * @return float
869
     */
870 9
    public static function estimateSize($txinSize, $txoutSize) {
871 9
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
872
    }
873
874
    /**
875
     * only supports estimating size for P2PKH/P2SH outputs
876
     *
877
     * @param int $outputCnt    number of outputs in transaction
878
     * @return float
879
     */
880 2
    public static function estimateSizeOutputs($outputCnt) {
881 2
        return ($outputCnt * 34);
882
    }
883
884
    /**
885
     * only supports estimating size for 2of3 multsig UTXOs
886
     *
887
     * @param int $utxoCnt      number of unspent inputs in transaction
888
     * @return float
889
     */
890 3
    public static function estimateSizeUTXOs($utxoCnt) {
891 3
        $txinSize = 0;
892
893 3
        for ($i=0; $i<$utxoCnt; $i++) {
894
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
895 3
            $multisig = "2of3";
896
897 3
            if ($multisig) {
898 3
                $sigCnt = 2;
899 3
                $msig = explode("of", $multisig);
900 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
901 3
                    $sigCnt = $msig[0];
902
                }
903
904 3
                $txinSize += array_sum([
905 3
                    32, // txhash
906 3
                    4, // idx
907 3
                    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...
908 3
                    ((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...
909
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
910
                    1, // OP_0
911 3
                    4, // sequence
912
                ]);
913
            } else {
914
                $txinSize += array_sum([
915
                    32, // txhash
916
                    4, // idx
917
                    73, // sig
918
                    34, // script
919
                    4, // sequence
920
                ]);
921
            }
922
        }
923
924 3
        return $txinSize;
925
    }
926
927
    /**
928
     * determine how much fee is required based on the inputs and outputs
929
     *  this is an estimation, not a proper 100% correct calculation
930
     *
931
     * @param UTXO[]  $utxos
932
     * @param array[] $outputs
933
     * @param         $feeStrategy
934
     * @param         $optimalFeePerKB
935
     * @param         $lowPriorityFeePerKB
936
     * @return int
937
     * @throws BlocktrailSDKException
938
     */
939 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
940
941 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
942
943
        switch ($feeStrategy) {
944 7
            case self::FEE_STRATEGY_BASE_FEE:
945 4
                return self::baseFeeForSize($size);
946
947 3
            case self::FEE_STRATEGY_OPTIMAL:
948 3
                return (int)round(($size / 1000) * $optimalFeePerKB);
949
950
            case self::FEE_STRATEGY_LOW_PRIORITY:
951
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
952
953
            default:
954
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
955
        }
956
    }
957
958
    /**
959
     * determine how much change is left over based on the inputs and outputs and the fee
960
     *
961
     * @param UTXO[]    $utxos
962
     * @param array[]   $outputs
963
     * @param int       $fee
964
     * @return int
965
     */
966 7
    protected function determineChange($utxos, $outputs, $fee) {
967
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
968 7
            return $utxo->value;
969 7
        }, $utxos));
970 7
        $outputsTotal = array_sum(array_column($outputs, 'value'));
971
972 7
        return $inputsTotal - $outputsTotal - $fee;
973
    }
974
975
    /**
976
     * sign a raw transaction with the private keys that we have
977
     *
978
     * @param Transaction $tx
979
     * @param SignInfo[]  $signInfo
980
     * @return TransactionInterface
981
     * @throws \Exception
982
     */
983 4
    protected function signTransaction(Transaction $tx, array $signInfo) {
984 4
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
985
986 4
        assert(Util::all(function ($signInfo) {
987 4
            return $signInfo instanceof SignInfo;
988 4
        }, $signInfo), '$signInfo should be SignInfo[]');
989
990 4
        $sigHash = SigHash::ALL;
991 4
        if ($this->network === "bitcoincash") {
992
            $sigHash |= SigHash::BITCOINCASH;
993
            $signer->redeemBitcoinCash(true);
994
        }
995
996 4
        foreach ($signInfo as $idx => $info) {
997 4
            if ($info->mode === SignInfo::MODE_SIGN) {
998
                // required SignInfo: path, redeemScript|witnessScript, output
999 4
                $path = BIP32Path::path($info->path)->privatePath();
1000 4
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1001 4
                $signData = new SignData();
1002 4
                if ($info->redeemScript) {
1003 4
                    $signData->p2sh($info->redeemScript);
1004
                }
1005 4
                if ($info->witnessScript) {
1006
                    $signData->p2wsh($info->witnessScript);
1007
                }
1008 4
                $input = $signer->input($idx, $info->output, $signData);
1009 4
                $input->sign($key, $sigHash);
1010
            }
1011
        }
1012
1013 4
        return $signer->get();
1014
    }
1015
1016
    /**
1017
     * send the transaction using the API
1018
     *
1019
     * @param string|array  $signed
1020
     * @param string[]      $paths
1021
     * @param bool          $checkFee
1022
     * @return string           the complete raw transaction
1023
     * @throws \Exception
1024
     */
1025 4
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1026 4
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1027
    }
1028
1029
    /**
1030
     * @param \array[] $outputs
1031
     * @param bool $lockUTXO
1032
     * @param bool $allowZeroConf
1033
     * @param int|null|string $feeStrategy
1034
     * @param null $forceFee
1035
     * @return array
1036
     */
1037 11
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1038 11
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1039
1040 5
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1041 5
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1042 5
        $this->feePerKBAge = time();
1043
1044 5
        return $result;
1045
    }
1046
1047 7
    public function getOptimalFeePerKB() {
1048 7
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1049 3
            $this->updateFeePerKB();
1050
        }
1051
1052 7
        return $this->optimalFeePerKB;
1053
    }
1054
1055 7
    public function getLowPriorityFeePerKB() {
1056 7
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1057
            $this->updateFeePerKB();
1058
        }
1059
1060 7
        return $this->lowPriorityFeePerKB;
1061
    }
1062
1063 3
    public function updateFeePerKB() {
1064 3
        $result = $this->sdk->feePerKB();
1065
1066 3
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1067 3
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1068
1069 3
        $this->feePerKBAge = time();
1070 3
    }
1071
1072
    /**
1073
     * delete the wallet
1074
     *
1075
     * @param bool $force ignore warnings (such as non-zero balance)
1076
     * @return mixed
1077
     * @throws \Exception
1078
     */
1079 10
    public function deleteWallet($force = false) {
1080 10
        if ($this->locked) {
1081
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1082
        }
1083
1084 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1085 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1086
    }
1087
1088
    /**
1089
     * create checksum to verify ownership of the master primary key
1090
     *
1091
     * @return string[]     [address, signature]
1092
     */
1093 10
    protected function createChecksumVerificationSignature() {
1094 10
        $privKey = $this->primaryPrivateKey->key();
1095
1096 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1097 10
        $address = $pubKey->getAddress()->getAddress();
1098
1099 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1100 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1101
1102 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1103
    }
1104
1105
    /**
1106
     * setup a webhook for our wallet
1107
     *
1108
     * @param string    $url            URL to receive webhook events
1109
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1110
     * @return array
1111
     */
1112 1
    public function setupWebhook($url, $identifier = null) {
1113 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1114 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1115
    }
1116
1117
    /**
1118
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1119
     * @return mixed
1120
     */
1121 1
    public function deleteWebhook($identifier = null) {
1122 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1123 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1124
    }
1125
1126
    /**
1127
     * lock a specific unspent output
1128
     *
1129
     * @param     $txHash
1130
     * @param     $txIdx
1131
     * @param int $ttl
1132
     * @return bool
1133
     */
1134
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1135
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1136
    }
1137
1138
    /**
1139
     * unlock a specific unspent output
1140
     *
1141
     * @param     $txHash
1142
     * @param     $txIdx
1143
     * @return bool
1144
     */
1145
    public function unlockUTXO($txHash, $txIdx) {
1146
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1147
    }
1148
1149
    /**
1150
     * get all transactions for the wallet (paginated)
1151
     *
1152
     * @param  integer $page    pagination: page number
1153
     * @param  integer $limit   pagination: records per page (max 500)
1154
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1155
     * @return array            associative array containing the response
1156
     */
1157 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1158 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1159
    }
1160
1161
    /**
1162
     * get all addresses for the wallet (paginated)
1163
     *
1164
     * @param  integer $page    pagination: page number
1165
     * @param  integer $limit   pagination: records per page (max 500)
1166
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1167
     * @return array            associative array containing the response
1168
     */
1169 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1170 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1171
    }
1172
1173
    /**
1174
     * get all UTXOs for the wallet (paginated)
1175
     *
1176
     * @param  integer $page        pagination: page number
1177
     * @param  integer $limit       pagination: records per page (max 500)
1178
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1179
     * @param  boolean $zeroconf    include zero confirmation transactions
1180
     * @return array                associative array containing the response
1181
     */
1182 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1183 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1184
    }
1185
}
1186