Completed
Pull Request — master (#85)
by thomas
69:21
created

Wallet::getWalletScriptFromKey()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 2
nop 2
dl 0
loc 24
ccs 7
cts 7
cp 1
crap 3
rs 8.9713
c 0
b 0
f 0
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\Classifier\OutputClassifier;
11
use BitWasp\Bitcoin\Script\ScriptFactory;
12
use BitWasp\Bitcoin\Script\ScriptInterface;
13
use BitWasp\Bitcoin\Script\WitnessScript;
14
use BitWasp\Bitcoin\Transaction\Factory\SignData;
15
use BitWasp\Bitcoin\Transaction\Factory\Signer;
16
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
17
use BitWasp\Bitcoin\Transaction\OutPoint;
18
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
19
use BitWasp\Bitcoin\Transaction\Transaction;
20
use BitWasp\Bitcoin\Transaction\TransactionInterface;
21
use BitWasp\Bitcoin\Transaction\TransactionOutput;
22
use BitWasp\Buffertools\Buffer;
23
use Blocktrail\SDK\Bitcoin\BIP32Key;
24
use Blocktrail\SDK\Bitcoin\BIP32Path;
25
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
26
27
/**
28
 * Class Wallet
29
 */
30
abstract class Wallet implements WalletInterface {
31
32
    const WALLET_VERSION_V1 = 'v1';
33
    const WALLET_VERSION_V2 = 'v2';
34
    const WALLET_VERSION_V3 = 'v3';
35
36
    const CHAIN_BTC_DEFAULT = 0;
37
    const CHAIN_BCC_DEFAULT = 1;
38
    const CHAIN_BTC_SEGWIT = 2;
39
40
    const BASE_FEE = 10000;
41
42
    /**
43
     * development / debug setting
44
     *  when getting a new derivation from the API,
45
     *  will verify address / redeeemScript with the values the API provides
46
     */
47
    const VERIFY_NEW_DERIVATION = true;
48
49
    /**
50
     * @var BlocktrailSDKInterface
51
     */
52
    protected $sdk;
53
54
    /**
55
     * @var string
56
     */
57
    protected $identifier;
58
59
    /**
60
     * BIP32 master primary private key (m/)
61
     *
62
     * @var BIP32Key
63
     */
64
    protected $primaryPrivateKey;
65
66
    /**
67
     * @var BIP32Key[]
68
     */
69
    protected $primaryPublicKeys;
70
71
    /**
72
     * BIP32 master backup public key (M/)
73
74
     * @var BIP32Key
75
     */
76
    protected $backupPublicKey;
77
78
    /**
79
     * map of blocktrail BIP32 public keys
80
     *  keyed by key index
81
     *  path should be `M / key_index'`
82
     *
83
     * @var BIP32Key[]
84
     */
85
    protected $blocktrailPublicKeys;
86
87
    /**
88
     * the 'Blocktrail Key Index' that is used for new addresses
89
     *
90
     * @var int
91
     */
92
    protected $keyIndex;
93
94
    /**
95
     * 'bitcoin'
96
     *
97
     * @var string
98
     */
99
    protected $network;
100
101
    /**
102
     * testnet yes / no
103
     *
104
     * @var bool
105
     */
106
    protected $testnet;
107
108
    /**
109
     * cache of public keys, by path
110
     *
111
     * @var BIP32Key[]
112
     */
113
    protected $pubKeys = [];
114
115
    /**
116
     * cache of address / redeemScript, by path
117
     *
118
     * @var string[][]      [[address, redeemScript)], ]
119
     */
120
    protected $derivations = [];
121
122
    /**
123
     * reverse cache of paths by address
124
     *
125
     * @var string[]
126
     */
127
    protected $derivationsByAddress = [];
128
129
    /**
130
     * @var WalletPath
131
     */
132
    protected $walletPath;
133
134
    protected $checksum;
135
136
    protected $locked = true;
137
138
    protected $optimalFeePerKB;
139
    protected $lowPriorityFeePerKB;
140
    protected $feePerKBAge;
141
142
    /**
143
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
144
     * @param string                        $identifier                 identifier of the wallet
145
     * @param BIP32Key[]                    $primaryPublicKeys
146
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
147 14
     * @param BIP32Key[]                    $blocktrailPublicKeys
148 14
     * @param int                           $keyIndex
149
     * @param string                        $network
150 14
     * @param bool                          $testnet
151 14
     * @param string                        $checksum
152 14
     */
153 14
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) {
154
        $this->sdk = $sdk;
155 14
156 14
        $this->identifier = $identifier;
157 14
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
158 14
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);;
159
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
160 14
161 14
        $this->network = $network;
162
        $this->testnet = $testnet;
163
        $this->keyIndex = $keyIndex;
164
        $this->checksum = $checksum;
165
166
        $this->setChainIndex();
167
    }
168 10
169 10
    /**
170
     * @return int
171
     */
172
    public function getDefaultChainIdx() {
173
        if ($this->network !== "bitcoincash") {
174
            return self::CHAIN_BTC_SEGWIT;
175
        } else {
176
            return self::CHAIN_BCC_DEFAULT;
177 5
        }
178
    }
179 5
180 5
    /**
181
     * Returns the current chain index, usually
182
     * indicating what type of scripts to derive.
183
     * @return int
184
     */
185
    public function getChainIndex() {
186
        return $this->walletPath->path()[2];
187
    }
188 10
189 10
    /**
190
     * @param null $chainIdx
191
     * @return $this
192
     */
193
    public function setChainIndex($chainIdx = null) {
194
        if (null === $chainIdx) {
195
            $chainIdx = $this->getDefaultChainIdx();
196
        }
197
198
        if (!in_array($chainIdx, [self::CHAIN_BTC_SEGWIT, self::CHAIN_BCC_DEFAULT, self::CHAIN_BTC_DEFAULT])) {
199 5
            throw new \RuntimeException("Unsupported chain index");
200 5
        }
201 4
202
        $this->walletPath = WalletPath::create($this->keyIndex, $chainIdx);
203
        return $this;
204 5
    }
205
206
    /**
207 5
     * return the wallet identifier
208
     *
209
     * @return string
210 5
     */
211
    public function getIdentifier() {
212 5
        return $this->identifier;
213
    }
214 5
215 5
    /**
216
     * return list of Blocktrail co-sign extended public keys
217
     *
218 5
     * @return array[]      [ [xpub, path] ]
219 5
     */
220 5
    public function getBlocktrailPublicKeys() {
221 5
        return array_map(function (BIP32Key $key) {
222 5
            return $key->tuple();
223
        }, $this->blocktrailPublicKeys);
224
    }
225
226 5
    /**
227
     * check if wallet is locked
228
     *
229
     * @return bool
230
     */
231
    public function isLocked() {
232
        return $this->locked;
233
    }
234
235
    /**
236 10
     * upgrade wallet to different blocktrail cosign key
237 10
     *
238
     * @param $keyIndex
239 10
     * @return bool
240 10
     * @throws \Exception
241
     */
242 10
    public function upgradeKeyIndex($keyIndex) {
243 10
        if ($this->locked) {
244 10
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
245
        }
246
247 10
        $walletPath = WalletPath::create($keyIndex);
248
249 10
        // do the upgrade to the new 'key_index'
250
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
251
252
        // $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...
253 10
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
254 10
255
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
256
257
        $this->keyIndex = $keyIndex;
258
        $this->walletPath = $walletPath;
259
260 10
        // update the blocktrail public keys
261
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
262
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
263
                $path = $pubKey[1];
264
                $pubKey = $pubKey[0];
265
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
266
            }
267
        }
268
269
        return true;
270 10
    }
271 10
272
    /**
273 10
     * get a new BIP32 derivation for the next (unused) address
274
     *  by requesting it from the API
275
     *
276
     * @return string
277 10
     * @throws \Exception
278
     */
279
    protected function getNewDerivation() {
280
        $path = $this->walletPath->path()->last("*");
281 10
        if (self::VERIFY_NEW_DERIVATION) {
282 10
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
283
284
            $path = $new['path'];
285 10
            $address = $new['address'];
286
            $redeemScript = $new['redeem_script'];
287
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
288
289
            /** @var ScriptInterface $checkRedeemScript */
290
            /** @var ScriptInterface $checkWitnessScript */
291
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
292
            if ($checkAddress != $address) {
293
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
294 10
            }
295 10
296 10
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
297 10
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
298
            }
299 10
300 10
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
301
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
302
            }
303 10
        } else {
304
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
305
        }
306
307
        return (string)$path;
308
    }
309
310
    /**
311
     * @param string|BIP32Path  $path
312 10
     * @return BIP32Key|false
313 10
     * @throws \Exception
314
     *
315
     * @TODO: hmm?
316 10
     */
317 10
    protected function getParentPublicKey($path) {
318
        $path = BIP32Path::path($path)->parent()->publicPath();
319
320
        if ($path->count() <= 2) {
321
            return false;
322 10
        }
323
324
        if ($path->isHardened()) {
325
            return false;
326
        }
327
328
        if (!isset($this->pubKeys[(string)$path])) {
329
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
330
        }
331
332
        return $this->pubKeys[(string)$path];
333
    }
334
335
    /**
336
     * get address for the specified path
337
     *
338
     * @param string|BIP32Path  $path
339
     * @return string
340 10
     */
341 10
    public function getAddressByPath($path) {
342
        $path = (string)BIP32Path::path($path)->privatePath();
343 10
        if (!isset($this->derivations[$path])) {
344
            list($address, ) = $this->getRedeemScriptByPath($path);
345 10
346 10
            $this->derivations[$path] = $address;
347 10
            $this->derivationsByAddress[$address] = $path;
348 10
        }
349 10
350
        return $this->derivations[$path];
351 10
    }
352
353
    /**
354
     * @param string $path
355
     * @return WalletScript
356
     */
357
    public function getWalletScriptByPath($path) {
358
        $path = BIP32Path::path($path);
359
360
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
361
        if ($pubKey = $this->getParentPublicKey($path)) {
362
            $key = $pubKey->buildKey($path->publicPath());
363
        } else {
364
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
365
        }
366
367
        return $this->getWalletScriptFromKey($key, $path);
368
    }
369 10
370 10
    /**
371
     * get address and redeemScript for specified path
372 10
     *
373
     * @param string    $path
374 10
     * @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...
375
     */
376
    public function getRedeemScriptByPath($path) {
377
        $walletScript = $this->getWalletScriptByPath($path);
378 10
379
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
380
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
381
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
382
    }
383
384
    /**
385
     * @param BIP32Key          $key
386 10
     * @param string|BIP32Path  $path
387 10
     * @return string
388 10
     */
389
    protected function getAddressFromKey(BIP32Key $key, $path) {
390 10
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
391
    }
392
393
    /**
394
     * @param BIP32Key          $key
395
     * @param string|BIP32Path  $path
396
     * @return WalletScript
397
     * @throws \Exception
398 7
     */
399 7
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
400
        $path = BIP32Path::path($path)->publicPath();
401
402
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
403
404
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
405
            $key->buildKey($path)->publicKey(),
406
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
407 9
            $blocktrailPublicKey->buildKey($path)->publicKey()
408 9
        ]), false);
409
410 9
        $type = (int)$key->path()[2];
411
        if ($this->network !== "bitcoincash" && $type === 2) {
412
            $witnessScript = new WitnessScript($multisig);
413
            $redeemScript = new P2shScript($witnessScript);
414
            $scriptPubKey = $redeemScript->getOutputScript();
415
        } else {
416
            $witnessScript = null;
417
            $redeemScript = new P2shScript($multisig);
418
            $scriptPubKey = $redeemScript->getOutputScript();
419 2
        }
420 2
421
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
422 2
    }
423
424
    /**
425
     * get the path (and redeemScript) to specified address
426
     *
427
     * @param string $address
428
     * @return array
429
     */
430
    public function getPathForAddress($address) {
431
        return $this->sdk->getPathForAddress($this->identifier, $address);
432
    }
433
434
    /**
435
     * @param string|BIP32Path  $path
436
     * @return BIP32Key
437
     * @throws \Exception
438 8
     */
439 8
    public function getBlocktrailPublicKey($path) {
440 4
        $path = BIP32Path::path($path);
441
442
        $keyIndex = str_replace("'", "", $path[1]);
443 8
444
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
445 8
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
446 8
        }
447 8
448 8
        return $this->blocktrailPublicKeys[$keyIndex];
449
    }
450 8
451 8
    /**
452
     * generate a new derived key and return the new path and address for it
453
     *
454 8
     * @return string[]     [path, address]
455
     */
456 2
    public function getNewAddressPair() {
457
        $path = $this->getNewDerivation();
458 2
        $address = $this->getAddressByPath($path);
459
460
        return [$path, $address];
461
    }
462
463
    /**
464
     * generate a new derived private key and return the new address for it
465
     *
466
     * @return string
467
     */
468
    public function getNewAddress() {
469
        return $this->getNewAddressPair()[1];
470
    }
471
472
    /**
473
     * get the balance for the wallet
474
     *
475
     * @return int[]            [confirmed, unconfirmed]
476
     */
477
    public function getBalance() {
478
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
479
480
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
481 9
    }
482 9
483
    /**
484 9
     * do wallet discovery (slow)
485 9
     *
486 1
     * @param int   $gap        the gap setting to use for discovery
487
     * @return int[]            [confirmed, unconfirmed]
488
     */
489
    public function doDiscovery($gap = 200) {
490 1
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
491 1
492 1
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
493 1
    }
494 1
495 1
    /**
496
     * create, sign and send a transaction
497 1
     *
498
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
499
     *                                      value should be INT
500 9
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
501 9
     * @param bool     $allowZeroConf
502
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
503
     * @param string   $feeStrategy
504 9
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
505
     * @return string the txid / transaction hash
506
     * @throws \Exception
507 9
     */
508
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
509
        if ($this->locked) {
510
            throw new \Exception("Wallet needs to be unlocked to pay");
511
        }
512
513
        $outputs = self::normalizeOutputsStruct($outputs);
514
515
        $txBuilder = new TransactionBuilder();
516
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
517
        $txBuilder->setFeeStrategy($feeStrategy);
518
        $txBuilder->setChangeAddress($changeAddress);
519 8
520
        foreach ($outputs as $output) {
521 8
            $txBuilder->addRecipient($output['address'], $output['value']);
522 2
        }
523 2
524 2
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
525
526 2
        $apiCheckFee = $forceFee === null;
527 1
528
        return $this->sendTx($txBuilder, $apiCheckFee);
529 2
    }
530
531
    /**
532 2
     * determine max spendable from wallet after fees
533 2
     *
534 2
     * @param bool     $allowZeroConf
535 2
     * @param string   $feeStrategy
536 2
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
537
     * @param int      $outputCnt
538
     * @return string
539 2
     * @throws BlocktrailSDKException
540
     */
541
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
542
        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...
543
    }
544
545
    /**
546
     * parse outputs into normalized struct
547
     *
548
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
549 3
     * @return array            [['address' => address, 'value' => value], ]
550 3
     */
551 3
    public static function normalizeOutputsStruct(array $outputs) {
552 3
        $result = [];
553
554 3
        foreach ($outputs as $k => $v) {
555
            if (is_numeric($k)) {
556 3
                if (!is_array($v)) {
557 3
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
558
                }
559
560
                if (isset($v['address']) && isset($v['value'])) {
561
                    $address = $v['address'];
562
                    $value = $v['value'];
563
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
564
                    $address = $v[0];
565
                    $value = $v[1];
566
                } else {
567
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
568
                }
569
            } else {
570
                $address = $k;
571
                $value = $v;
572
            }
573
574
            $result[] = ['address' => $address, 'value' => $value];
575
        }
576
577 3
        return $result;
578
    }
579
580
    /**
581 3
     * 'fund' the txBuilder with UTXOs (modified in place)
582
     *
583
     * @param TransactionBuilder    $txBuilder
584
     * @param bool|true             $lockUTXOs
585
     * @param bool|false            $allowZeroConf
586 3
     * @param null|int              $forceFee
587
     * @return TransactionBuilder
588
     */
589
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
590 3
        // get the data we should use for this transaction
591 3
        $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 589 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...
592 3
        $utxos = $coinSelection['utxos'];
593 1
        $fee = $coinSelection['fee'];
594
        $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...
595
596 3
        if ($forceFee !== null) {
597
            $txBuilder->setFee($forceFee);
598 3
        } else {
599 2
            $txBuilder->validateFee($fee);
600
        }
601
602
        foreach ($utxos as $utxo) {
603
            $scriptPubKey = ScriptFactory::fromHex($utxo['scriptpubkey_hex']);
604 3
            $redeemScript = ScriptFactory::fromHex($utxo['redeem_script']);
605 3
            $address = AddressFactory::fromString($utxo['address']);
606 3
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $address, $scriptPubKey, $utxo['path'], $redeemScript);
607 3
        }
608
609
        return $txBuilder;
610
    }
611 3
612 3
    /**
613
     * build inputs and outputs lists for TransactionBuilder
614
     *
615
     * @param TransactionBuilder $txBuilder
616 3
     * @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...
617 3
     * @throws \Exception
618
     */
619
    public function buildTx(TransactionBuilder $txBuilder) {
620 3
        $send = $txBuilder->getOutputs();
621 3
        $utxos = $txBuilder->getUtxos();
622
        $signInfo = [];
623 3
624 1
        $txb = new TxBuilder();
625 3
626 3
        foreach ($utxos as $utxo) {
627
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
628 3
                $tx = $this->sdk->transaction($utxo->hash);
629
630
                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...
631
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
632 3
                }
633
634
                $output = $tx['outputs'][$utxo->index];
635 3
636 3
                if (!$utxo->address) {
637 3
                    $utxo->address = AddressFactory::fromString($output['address']);
638
                }
639 3
                if (!$utxo->value) {
640 3
                    $utxo->value = $output['value'];
641
                }
642
                if (!$utxo->scriptPubKey) {
643 3
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
644 2
                }
645
            }
646
647 2
            if (!$utxo->path) {
648
                $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
649
            }
650
651 2
            if (!$utxo->redeemScript || !$utxo->witnessScript) {
652
                list(, $redeemScript, $witnessScript) = $this->getRedeemScriptByPath($utxo->path);
653
                $utxo->redeemScript = $redeemScript;
654 3
                $utxo->witnessScript = $witnessScript;
0 ignored issues
show
Documentation Bug introduced by
It seems like $witnessScript can also be of type object<BitWasp\Bitcoin\Script\WitnessScript>. However, the property $witnessScript is declared as type null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
655
            }
656 3
657
            $signInfo[] = new SignInfo($utxo->path, $utxo->redeemScript, $utxo->witnessScript, new TransactionOutput($utxo->value, $utxo->scriptPubKey));
0 ignored issues
show
Bug introduced by
It seems like $utxo->redeemScript can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
658 3
        }
659 3
660
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
661 3
            return $utxo->value;
662
        }, $utxos));
663
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
664 3
            throw new \Exception("Atempting to spend more than sum of UTXOs");
665
        }
666
667 3
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
668
669
        if ($txBuilder->getValidateFee() !== null) {
670
            if (abs($txBuilder->getValidateFee() - $fee) > Wallet::BASE_FEE) {
671 3
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
672 1
            }
673
        }
674 3
675
        if ($change > 0) {
676
            $send[] = [
677 3
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewAddress(),
678 3
                'value' => $change
679
            ];
680
        }
681
682
        foreach ($utxos as $utxo) {
683
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index), $utxo->scriptPubKey);
684
        }
685
686
        // outputs should be randomized to make the change harder to detect
687 3
        if ($txBuilder->shouldRandomizeChangeOuput()) {
688
            shuffle($send);
689 3
        }
690
691
        foreach ($send as $out) {
692
            assert(isset($out['value']));
693
694
            if (isset($out['scriptPubKey'])) {
695
                $txb->output($out['value'], $out['scriptPubKey']);
696
            } elseif (isset($out['address'])) {
697
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
698
            } else {
699
                throw new \Exception();
700 2
            }
701 2
        }
702
703 2
        return [$txb->get(), $signInfo];
704
    }
705
706
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) {
707
        $send = $txBuilder->getOutputs();
708
        $utxos = $txBuilder->getUtxos();
709
710
        $fee = $txBuilder->getFee();
711
        $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...
712
713
        // if the fee is fixed we just need to calculate the change
714
        if ($fee !== null) {
715
            $change = $this->determineChange($utxos, $send, $fee);
716
717 2
            // if change is not dust we need to add a change output
718 2
            if ($change > Blocktrail::DUST) {
719
                $send[] = ['address' => 'change', 'value' => $change];
720
            } else {
721
                // if change is dust we do nothing (implicitly it's added to the fee)
722
                $change = 0;
723 2
            }
724 2
        } else {
725
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
726
727 2
            $change = $this->determineChange($utxos, $send, $fee);
728
729
            if ($change > 0) {
730
                $changeIdx = count($send);
731 2
                // set dummy change output
732 2
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
733
734 2
                // recaculate fee now that we know that we have a change output
735
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
736
737
                // unset dummy change output
738
                unset($send[$changeIdx]);
739
740
                // if adding the change output made the fee bump up and the change is smaller than the fee
741
                //  then we're not doing change
742
                if ($fee2 > $fee && $fee2 > $change) {
743
                    $change = 0;
744
                } else {
745 1
                    $change = $this->determineChange($utxos, $send, $fee2);
746 1
747
                    // if change is not dust we need to add a change output
748 1
                    if ($change > Blocktrail::DUST) {
749
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
750
                    } else {
751
                        // if change is dust we do nothing (implicitly it's added to the fee)
752
                        $change = 0;
753
                    }
754
                }
755 3
            }
756 3
        }
757
758 3
        $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $optimalFeePerKB, $lowPriorityFeePerKB);
759
760
        return [$fee, $change];
761
    }
762
763
    /**
764
     * create, sign and send transction based on TransactionBuilder
765
     *
766 5
     * @param TransactionBuilder $txBuilder
767 5
     * @param bool $apiCheckFee     let the API check if the fee is correct
768
     * @return string
769
     * @throws \Exception
770
     */
771
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
772
        list($tx, $signInfo) = $this->buildTx($txBuilder);
773
774
        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...
775
    }
776 2
777 2
    /**
778
     * !! INTERNAL METHOD, public for testing purposes !!
779
     * create, sign and send transction based on inputs and outputs
780
     *
781
     * @param Transaction $tx
782
     * @param SignInfo[]  $signInfo
783
     * @param bool $apiCheckFee     let the API check if the fee is correct
784
     * @return string
785
     * @throws \Exception
786 5
     * @internal
787 5
     */
788
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
789 5
        if ($this->locked) {
790
            throw new \Exception("Wallet needs to be unlocked to pay");
791 5
        }
792
793 5
        assert(Util::all(function ($signInfo) {
794 5
            return $signInfo instanceof SignInfo;
795 5
        }, $signInfo), '$signInfo should be SignInfo[]');
796 5
797 5
        // sign the transaction with our keys
798
        $signed = $this->signTransaction($tx, $signInfo);
799
800 5
        $txs = [
801 5
            'signed_transaction' => $signed->getHex(),
802 5
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
803 5
        ];
804 5
805
        // send the transaction
806
        $finished = $this->sendTransaction($txs, array_map(function (SignInfo $r) {
0 ignored issues
show
Documentation introduced by
$txs is of type array<string,?,{"signed_...base_transaction":"?"}>, 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...
807 5
            return (string)$r->path;
808
        }, $signInfo), $apiCheckFee);
809
810
        return $finished;
811
    }
812
813
    /**
814
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
815
     *
816
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
817
     * @param int $utxoCnt      number of unspent inputs in transaction
818
     * @param int $outputCnt    number of outputs in transaction
819
     * @return float
820 5
     * @access public           reminder that people might use this!
821
     */
822
    public static function estimateFee($utxoCnt, $outputCnt) {
823
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
824
825
        return self::baseFeeForSize($size);
826
    }
827
828
    /**
829
     * @param int $size     size in bytes
830
     * @return int          fee in satoshi
831
     */
832
    public static function baseFeeForSize($size) {
833
        $sizeKB = (int)ceil($size / 1000);
834
835 3
        return $sizeKB * self::BASE_FEE;
836 3
    }
837 3
838 3
    /**
839 1
     * @param int $txinSize
840 1
     * @param int $txoutSize
841
     * @return float
842 1
     */
843
    public static function estimateSize($txinSize, $txoutSize) {
844
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
845 3
    }
846
847
    /**
848
     * only supports estimating size for P2PKH/P2SH outputs
849 3
     *
850
     * @param int $outputCnt    number of outputs in transaction
851
     * @return float
852 3
     */
853 2
    public static function estimateSizeOutputs($outputCnt) {
854
        return ($outputCnt * 34);
855 2
    }
856 2
857
    /**
858 1
     * only supports estimating size for 2of3 multsig UTXOs
859 1
     *
860
     * @param int $utxoCnt      number of unspent inputs in transaction
861
     * @return float
862
     */
863
    public static function estimateSizeUTXOs($utxoCnt) {
864
        $txinSize = 0;
865
866
        for ($i=0; $i<$utxoCnt; $i++) {
867
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
868
            $multisig = "2of3";
869
870
            if ($multisig) {
871
                $sigCnt = 2;
872
                $msig = explode("of", $multisig);
873
                if (count($msig) == 2 && is_numeric($msig[0])) {
874 3
                    $sigCnt = $msig[0];
875
                }
876 3
877 3
                $txinSize += array_sum([
878 3
                    32, // txhash
879
                    4, // idx
880 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...
881
                    ((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...
882
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
883
                    1, // OP_0
884
                    4, // sequence
885
                ]);
886
            } else {
887
                $txinSize += array_sum([
888
                    32, // txhash
889
                    4, // idx
890
                    73, // sig
891 2
                    34, // script
892 2
                    4, // sequence
893
                ]);
894 2
            }
895 2
        }
896 2
897
        return $txinSize;
898 2
    }
899 2
900
    /**
901
     * @param UTXO[] $utxos
902
     * @param bool $withWitness
903
     * @return integer
904 2
     */
905 2
    public static function estimateSpendSize(array $utxos, $withWitness)
906 2
    {
907 2
        $inputSize = 0;
908
        $witnessSize = 0;
909 2
        foreach ($utxos as $utxo) {
910
            $estimate = SizeEstimation::estimateUtxo($utxo);
911 2
            $inputSize += 32 + 4 + 4;
912 2
            $inputSize += $estimate['scriptSig'];
913
            if ($withWitness) {
914
                $witnessSize += $estimate['witness'];
915 2
            }
916
        }
917
918
        if ($withWitness && $witnessSize != 0) {
919
            $inputSize += $witnessSize;
920
            $inputSize += 2; // flag bytes
921
        }
922
923
        return $inputSize;
924
    }
925
926
    /**
927 2
     * @param array $utxos
928 2
     * @param int $outputSize
929
     * @return int
930
     */
931
    public static function estimateWeight(array $utxos, $outputSize) {
932
        $baseSize = self::estimateSize(self::estimateSpendSize($utxos, false), $outputSize);
933
        $witnessSize = self::estimateSize(self::estimateSpendSize($utxos, true), $outputSize);
934
935
        return ($baseSize * 3) + $witnessSize;
936
    }
937
938
    /**
939 8
     * @param array $utxos
940 8
     * @param $outputSize
941
     * @return int
942 2
     */
943 2
    public function estimateVsize(array $utxos, $outputSize) {
944 2
        return (int) ceil(self::estimateWeight($utxos, $outputSize) / 4);
945
    }
946 2
947
    /**
948
     * determine how much fee is required based on the inputs and outputs
949 3
     *  this is an estimation, not a proper 100% correct calculation
950 3
     *
951 1
     * @param UTXO[]  $utxos
952
     * @param array[] $outputs
953
     * @param         $feeStrategy
954 3
     * @param         $optimalFeePerKB
955
     * @param         $lowPriorityFeePerKB
956
     * @return int
957 3
     * @throws BlocktrailSDKException
958 3
     */
959
    protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) {
960
        $outputSize = 0;
961
        foreach ($outputs as $output) {
962 3
            if (isset($output['scriptPubKey'])) {
963
                if ($output['scriptPubKey'] instanceof ScriptInterface) {
964
                    $outputSize += $output['scriptPubKey']->getBuffer()->getSize();
965 1
                } else {
966 1
                    $outputSize += strlen($output['scriptPubKey']) / 2; // asume HEX
967
                }
968 1
            } else {
969 1
                $outputSize += 34;
970
            }
971 1
        }
972 1
973
        $size = self::estimateVsize($utxos, $outputSize);
974
975
        switch ($feeStrategy) {
976
            case self::FEE_STRATEGY_BASE_FEE:
977
                return self::baseFeeForSize($size);
978
979
            case self::FEE_STRATEGY_OPTIMAL:
980
                return (int)round(($size / 1000) * $optimalFeePerKB);
981 10
982 10
            case self::FEE_STRATEGY_LOW_PRIORITY:
983
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
984
985
            default:
986 10
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
987 10
        }
988
    }
989
990
    /**
991
     * determine how much change is left over based on the inputs and outputs and the fee
992
     *
993
     * @param UTXO[]    $utxos
994
     * @param array[]   $outputs
995 10
     * @param int       $fee
996 10
     * @return int
997
     */
998 10
    protected function determineChange($utxos, $outputs, $fee) {
999 10
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
1000
            return $utxo->value;
1001 10
        }, $utxos));
1002 10
        $outputsTotal = array_sum(array_column($outputs, 'value'));
1003
1004 10
        return $inputsTotal - $outputsTotal - $fee;
1005
    }
1006
1007
    /**
1008
     * sign a raw transaction with the private keys that we have
1009
     *
1010
     * @param Transaction $tx
1011
     * @param SignInfo[]  $signInfo
1012
     * @return TransactionInterface
1013
     * @throws \Exception
1014 1
     */
1015 1
    protected function signTransaction(Transaction $tx, array $signInfo) {
1016 1
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1017
1018
        assert(Util::all(function ($signInfo) {
1019
            return $signInfo instanceof SignInfo;
1020
        }, $signInfo), '$signInfo should be SignInfo[]');
1021
1022
        $sigHash = SigHash::ALL;
1023 1
        if ($this->network === "bitcoincash") {
1024 1
            $sigHash |= SigHash::BITCOINCASH;
1025 1
            $signer->redeemBitcoinCash(true);
1026
        }
1027
1028
        foreach ($signInfo as $idx => $info) {
1029
            $path = BIP32Path::path($info->path)->privatePath();
1030
            $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1031
1032
            $signData = new SignData();
1033
            if ($info->redeemScript) {
1034
                $signData->p2sh($info->redeemScript);
1035
            }
1036
            if ($info->witnessScript) {
1037
                $signData->p2wsh($info->witnessScript);
1038
            }
1039
1040
            $input = $signer->input($idx, $info->output, $signData);
1041
            $input->sign($key, $sigHash);
1042
        }
1043
1044
        return $signer->get();
1045
    }
1046
1047
    /**
1048
     * send the transaction using the API
1049
     *
1050
     * @param string    $signed
1051
     * @param string[]  $paths
1052
     * @param bool      $checkFee
1053
     * @return string           the complete raw transaction
1054
     * @throws \Exception
1055
     */
1056
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1057
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1058
    }
1059 1
1060 1
    /**
1061
     * @param \array[] $outputs
1062
     * @param bool $lockUTXO
1063
     * @param bool $allowZeroConf
1064
     * @param int|null|string $feeStrategy
1065
     * @param null $forceFee
1066
     * @return array
1067
     */
1068
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1069
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1070
1071 1
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1072 1
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1073
        $this->feePerKBAge = time();
1074
1075
        return $result;
1076
    }
1077
1078
    public function getOptimalFeePerKB() {
1079
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1080
            $this->updateFeePerKB();
1081
        }
1082
1083
        return $this->optimalFeePerKB;
1084 1
    }
1085 1
1086
    public function getLowPriorityFeePerKB() {
1087
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1088
            $this->updateFeePerKB();
1089
        }
1090
1091
        return $this->lowPriorityFeePerKB;
1092
    }
1093
1094
    public function updateFeePerKB() {
1095
        $result = $this->sdk->feePerKB();
1096
1097
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1098
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1099
1100
        $this->feePerKBAge = time();
1101
    }
1102
1103
    /**
1104
     * delete the wallet
1105
     *
1106
     * @param bool $force ignore warnings (such as non-zero balance)
1107
     * @return mixed
1108
     * @throws \Exception
1109
     */
1110
    public function deleteWallet($force = false) {
1111
        if ($this->locked) {
1112
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1113
        }
1114
1115
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1116
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1117
    }
1118
1119
    /**
1120
     * create checksum to verify ownership of the master primary key
1121
     *
1122
     * @return string[]     [address, signature]
1123
     */
1124
    protected function createChecksumVerificationSignature() {
1125
        $privKey = $this->primaryPrivateKey->key();
1126
1127
        $pubKey = $this->primaryPrivateKey->publicKey();
1128
        $address = $pubKey->getAddress()->getAddress();
1129
1130
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1131
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1132
1133
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1134
    }
1135
1136
    /**
1137
     * setup a webhook for our wallet
1138
     *
1139
     * @param string    $url            URL to receive webhook events
1140
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1141
     * @return array
1142
     */
1143
    public function setupWebhook($url, $identifier = null) {
1144
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1145
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1146
    }
1147
1148
    /**
1149
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1150
     * @return mixed
1151
     */
1152
    public function deleteWebhook($identifier = null) {
1153
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1154
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1155
    }
1156
1157
    /**
1158
     * lock a specific unspent output
1159
     *
1160
     * @param     $txHash
1161
     * @param     $txIdx
1162
     * @param int $ttl
1163
     * @return bool
1164
     */
1165
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1166
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1167
    }
1168
1169
    /**
1170
     * unlock a specific unspent output
1171
     *
1172
     * @param     $txHash
1173
     * @param     $txIdx
1174
     * @return bool
1175
     */
1176
    public function unlockUTXO($txHash, $txIdx) {
1177
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1178
    }
1179
1180
    /**
1181
     * get all transactions for the wallet (paginated)
1182
     *
1183
     * @param  integer $page    pagination: page number
1184
     * @param  integer $limit   pagination: records per page (max 500)
1185
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1186
     * @return array            associative array containing the response
1187
     */
1188
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1189
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1190
    }
1191
1192
    /**
1193
     * get all addresses for the wallet (paginated)
1194
     *
1195
     * @param  integer $page    pagination: page number
1196
     * @param  integer $limit   pagination: records per page (max 500)
1197
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1198
     * @return array            associative array containing the response
1199
     */
1200
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1201
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1202
    }
1203
1204
    /**
1205
     * get all UTXOs for the wallet (paginated)
1206
     *
1207
     * @param  integer $page        pagination: page number
1208
     * @param  integer $limit       pagination: records per page (max 500)
1209
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1210
     * @param  boolean $zeroconf    include zero confirmation transactions
1211
     * @return array                associative array containing the response
1212
     */
1213
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1214
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1215
    }
1216
}
1217