Completed
Pull Request — master (#99)
by thomas
24:31
created

Wallet::getNewAddressPair()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\Base58AddressInterface;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Address\ScriptHashAddress;
8
use BitWasp\Bitcoin\Bitcoin;
9
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
10
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
11
use BitWasp\Bitcoin\Script\P2shScript;
12
use BitWasp\Bitcoin\Script\ScriptFactory;
13
use BitWasp\Bitcoin\Script\ScriptInterface;
14
use BitWasp\Bitcoin\Script\ScriptType;
15
use BitWasp\Bitcoin\Script\WitnessScript;
16
use BitWasp\Bitcoin\Transaction\Factory\SignData;
17
use BitWasp\Bitcoin\Transaction\Factory\Signer;
18
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
19
use BitWasp\Bitcoin\Transaction\OutPoint;
20
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
21
use BitWasp\Bitcoin\Transaction\Transaction;
22
use BitWasp\Bitcoin\Transaction\TransactionInterface;
23
use BitWasp\Buffertools\Buffer;
24
use Blocktrail\SDK\Address\AddressReaderBase;
25
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
26
use Blocktrail\SDK\Address\CashAddress;
27
use Blocktrail\SDK\Bitcoin\BIP32Key;
28
use Blocktrail\SDK\Bitcoin\BIP32Path;
29
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
30
31
/**
32
 * Class Wallet
33
 */
34
abstract class Wallet implements WalletInterface {
35
36
    const WALLET_VERSION_V1 = 'v1';
37
    const WALLET_VERSION_V2 = 'v2';
38
    const WALLET_VERSION_V3 = 'v3';
39
40
    const CHAIN_BTC_DEFAULT = 0;
41
    const CHAIN_BCC_DEFAULT = 1;
42
    const CHAIN_BTC_SEGWIT = 2;
43
44
    const BASE_FEE = 10000;
45
46
    /**
47
     * development / debug setting
48
     *  when getting a new derivation from the API,
49
     *  will verify address / redeeemScript with the values the API provides
50
     */
51
    const VERIFY_NEW_DERIVATION = true;
52
53
    /**
54
     * @var BlocktrailSDKInterface
55
     */
56
    protected $sdk;
57
58
    /**
59
     * @var string
60
     */
61
    protected $identifier;
62
63
    /**
64
     * BIP32 master primary private key (m/)
65
     *
66
     * @var BIP32Key
67
     */
68
    protected $primaryPrivateKey;
69
70
    /**
71
     * @var BIP32Key[]
72
     */
73
    protected $primaryPublicKeys;
74
75
    /**
76
     * BIP32 master backup public key (M/)
77
78
     * @var BIP32Key
79
     */
80
    protected $backupPublicKey;
81
82
    /**
83
     * map of blocktrail BIP32 public keys
84
     *  keyed by key index
85
     *  path should be `M / key_index'`
86
     *
87
     * @var BIP32Key[]
88
     */
89
    protected $blocktrailPublicKeys;
90
91
    /**
92
     * the 'Blocktrail Key Index' that is used for new addresses
93
     *
94
     * @var int
95
     */
96
    protected $keyIndex;
97
98
    /**
99
     * 'bitcoin'
100
     *
101
     * @var string
102
     */
103
    protected $network;
104
105
    /**
106
     * testnet yes / no
107
     *
108
     * @var bool
109
     */
110
    protected $testnet;
111
112
    /**
113
     * cache of public keys, by path
114
     *
115
     * @var BIP32Key[]
116
     */
117
    protected $pubKeys = [];
118
119
    /**
120
     * cache of address / redeemScript, by path
121
     *
122
     * @var string[][]      [[address, redeemScript)], ]
123
     */
124
    protected $derivations = [];
125
126
    /**
127
     * reverse cache of paths by address
128
     *
129
     * @var string[]
130
     */
131
    protected $derivationsByAddress = [];
132
133
    /**
134
     * @var string
135
     */
136
    protected $checksum;
137
138
    /**
139
     * @var bool
140
     */
141
    protected $locked = true;
142
143
    /**
144
     * @var bool
145
     */
146
    protected $isSegwit = false;
147
148
    /**
149
     * @var int
150
     */
151
    protected $chainIndex;
152
153
    /**
154
     * @var int
155
     */
156
    protected $changeIndex;
157
158
    /**
159
     * @var AddressReaderBase
160
     */
161
    protected $addressReader;
162
163
    protected $highPriorityFeePerKB;
164
    protected $optimalFeePerKB;
165
    protected $lowPriorityFeePerKB;
166
    protected $feePerKBAge;
167
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
168
169
    /**
170
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
171
     * @param string                        $identifier                 identifier of the wallet
172
     * @param BIP32Key[]                    $primaryPublicKeys
173
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
174
     * @param BIP32Key[]                    $blocktrailPublicKeys
175
     * @param int                           $keyIndex
176
     * @param string                        $network
177
     * @param bool                          $testnet
178
     * @param bool                          $segwit
179
     * @param string                        $checksum
180
     * @throws BlocktrailSDKException
181
     */
182 26
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, AddressReaderBase $addressReader, $checksum) {
183 26
        $this->sdk = $sdk;
184
185 26
        $this->identifier = $identifier;
186 26
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
187 26
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
188 26
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
189
190 26
        $this->network = $network;
191 26
        $this->testnet = $testnet;
192 26
        $this->keyIndex = $keyIndex;
193 26
        $this->checksum = $checksum;
194
195 26
        if ($network === "bitcoin") {
196 22
            if ($segwit) {
197 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
198 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
199
            } else {
200 20
                $chainIdx = self::CHAIN_BTC_DEFAULT;
201 22
                $changeIdx = self::CHAIN_BTC_DEFAULT;
202
            }
203
        } else {
204 4
            if ($segwit && $network === "bitcoincash") {
205
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
206
            }
207 4
            $chainIdx = self::CHAIN_BCC_DEFAULT;
208 4
            $changeIdx = self::CHAIN_BCC_DEFAULT;
209
        }
210
211 26
        $this->addressReader = $addressReader;
212 26
        $this->isSegwit = (bool) $segwit;
213 26
        $this->chainIndex = $chainIdx;
214 26
        $this->changeIndex = $changeIdx;
215 26
    }
216
217
    /**
218
     * @return AddressReaderBase
219
     */
220 17
    public function getAddressReader() {
221 17
        return $this->addressReader;
222
    }
223
224
    /**
225
     * @param int|null $chainIndex
226
     * @return WalletPath
227
     * @throws BlocktrailSDKException
228
     */
229 17
    protected function getWalletPath($chainIndex = null) {
230 17
        if ($chainIndex === null) {
231 14
            return WalletPath::create($this->keyIndex, $this->chainIndex);
232
        } else {
233 7
            if (!is_int($chainIndex)) {
234 1
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
235
            }
236 6
            return WalletPath::create($this->keyIndex, $chainIndex);
237
        }
238
    }
239
240
    /**
241
     * @return bool
242
     */
243 3
    public function isSegwit() {
244 3
        return $this->isSegwit;
245
    }
246
247
    /**
248
     * return the wallet identifier
249
     *
250
     * @return string
251
     */
252 10
    public function getIdentifier() {
253 10
        return $this->identifier;
254
    }
255
256
    /**
257
     * Returns the wallets backup public key
258
     *
259
     * @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...
260
     */
261 1
    public function getBackupKey() {
262 1
        return $this->backupPublicKey->tuple();
263
    }
264
265
    /**
266
     * return list of Blocktrail co-sign extended public keys
267
     *
268
     * @return array[]      [ [xpub, path] ]
269
     */
270 5
    public function getBlocktrailPublicKeys() {
271
        return array_map(function (BIP32Key $key) {
272 5
            return $key->tuple();
273 5
        }, $this->blocktrailPublicKeys);
274
    }
275
276
    /**
277
     * check if wallet is locked
278
     *
279
     * @return bool
280
     */
281 10
    public function isLocked() {
282 10
        return $this->locked;
283
    }
284
285
    /**
286
     * upgrade wallet to different blocktrail cosign key
287
     *
288
     * @param $keyIndex
289
     * @return bool
290
     * @throws \Exception
291
     */
292 5
    public function upgradeKeyIndex($keyIndex) {
293 5
        if ($this->locked) {
294 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
295
        }
296
297 5
        $walletPath = WalletPath::create($keyIndex);
298
299
        // do the upgrade to the new 'key_index'
300 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
301
302
        // $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...
303 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
304
305 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
306 5
        $this->keyIndex = $keyIndex;
307
308
        // update the blocktrail public keys
309 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
310 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
311 5
                $path = $pubKey[1];
312 5
                $pubKey = $pubKey[0];
313 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
314
            }
315
        }
316
317 5
        return true;
318
    }
319
320
    /**
321
     * get a new BIP32 derivation for the next (unused) address
322
     *  by requesting it from the API
323
     *
324
     * @return string
325
     * @param int|null $chainIndex
326
     * @throws \Exception
327
     */
328 17
    protected function getNewDerivation($chainIndex = null) {
329 17
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
330
331 16
        if (self::VERIFY_NEW_DERIVATION) {
332 16
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
333
334 16
            $path = $new['path'];
335 16
            $address = $new['address'];
336
337 16
            $serverDecoded = $this->addressReader->fromString($address);
338
339 16
            $redeemScript = $new['redeem_script'];
340 16
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
341
342
            /** @var ScriptInterface $checkRedeemScript */
343
            /** @var ScriptInterface $checkWitnessScript */
344 16
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
345
346 16
            $oursDecoded = $this->addressReader->fromString($checkAddress);
347
348 16
            if ($this->network === "bitcoincash" &&
349 16
                $serverDecoded instanceof Base58AddressInterface &&
350 16
                $oursDecoded instanceof CashAddress
351
            ) {
352
                // our address is a cashaddr, server gave us base58.
353
354 1
                if (!$oursDecoded->getHash()->equals($serverDecoded->getHash())) {
355
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [hash mismatch]");
356
                }
357
358 1
                $matchedP2PKH = $serverDecoded instanceof PayToPubKeyHashAddress && $oursDecoded->getType() === ScriptType::P2PKH;
359 1
                $matchedP2SH = $serverDecoded instanceof ScriptHashAddress && $oursDecoded->getType() === ScriptType::P2SH;
360 1
                if (!($matchedP2PKH || $matchedP2SH)) {
361
                    throw new BlocktrailSDKException("Failed to verify legacy address from server [prefix mismatch]");
362
                }
363
364
                // promote the legacy address to our cashaddr, as they are equivalent.
365 1
                $address = $checkAddress;
366
            }
367
368 16
            if ($checkAddress != $address) {
369
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
370
            }
371
372 16
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
373
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
374
            }
375
376 16
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
377 16
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
378
            }
379
        } else {
380
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
381
        }
382
383 16
        return (string)$path;
384
    }
385
386
    /**
387
     * @param string|BIP32Path  $path
388
     * @return BIP32Key|false
389
     * @throws \Exception
390
     *
391
     * @TODO: hmm?
392
     */
393 18
    protected function getParentPublicKey($path) {
394 18
        $path = BIP32Path::path($path)->parent()->publicPath();
395
396 18
        if ($path->count() <= 2) {
397
            return false;
398
        }
399
400 18
        if ($path->isHardened()) {
401
            return false;
402
        }
403
404 18
        if (!isset($this->pubKeys[(string)$path])) {
405 18
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
406
        }
407
408 18
        return $this->pubKeys[(string)$path];
409
    }
410
411
    /**
412
     * get address for the specified path
413
     *
414
     * @param string|BIP32Path  $path
415
     * @return string
416
     */
417 16
    public function getAddressByPath($path) {
418 16
        $path = (string)BIP32Path::path($path)->privatePath();
419 16
        if (!isset($this->derivations[$path])) {
420 16
            list($address, ) = $this->getRedeemScriptByPath($path);
421
422 16
            $this->derivations[$path] = $address;
423 16
            $this->derivationsByAddress[$address] = $path;
424
        }
425
426 16
        return $this->derivations[$path];
427
    }
428
429
    /**
430
     * @param string $path
431
     * @return WalletScript
432
     */
433 18
    public function getWalletScriptByPath($path) {
434 18
        $path = BIP32Path::path($path);
435
436
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
437 18
        if ($pubKey = $this->getParentPublicKey($path)) {
438 18
            $key = $pubKey->buildKey($path->publicPath());
439
        } else {
440
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
441
        }
442
443 18
        return $this->getWalletScriptFromKey($key, $path);
444
    }
445
446
    /**
447
     * get address and redeemScript for specified path
448
     *
449
     * @param string    $path
450
     * @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...
451
     */
452 17
    public function getRedeemScriptByPath($path) {
453 17
        $walletScript = $this->getWalletScriptByPath($path);
454
455 17
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
456 17
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
457 17
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
458
    }
459
460
    /**
461
     * @param BIP32Key          $key
462
     * @param string|BIP32Path  $path
463
     * @return string
464
     */
465
    protected function getAddressFromKey(BIP32Key $key, $path) {
466
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
467
    }
468
469
    /**
470
     * @param BIP32Key          $key
471
     * @param string|BIP32Path  $path
472
     * @return WalletScript
473
     * @throws \Exception
474
     */
475 18
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
476 18
        $path = BIP32Path::path($path)->publicPath();
477
478 18
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
479
480 18
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
481 18
            $key->buildKey($path)->publicKey(),
482 18
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
483 18
            $blocktrailPublicKey->buildKey($path)->publicKey()
484 18
        ]), false);
485
486 18
        $type = (int)$key->path()[2];
487 18
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
488 3
            $witnessScript = new WitnessScript($multisig);
489 3
            $redeemScript = new P2shScript($witnessScript);
490 3
            $scriptPubKey = $redeemScript->getOutputScript();
491 18
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
492 17
            $witnessScript = null;
493 17
            $redeemScript = new P2shScript($multisig);
494 17
            $scriptPubKey = $redeemScript->getOutputScript();
495
        } else {
496 1
            throw new BlocktrailSDKException("Unsupported chain in path");
497
        }
498
499 17
        $address = $this->addressReader->fromOutputScript($scriptPubKey);
500
501 17
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript, $address);
502
    }
503
504
    /**
505
     * get the path (and redeemScript) to specified address
506
     *
507
     * @param string $address
508
     * @return array
509
     */
510 1
    public function getPathForAddress($address) {
511 1
        $decoded = $this->addressReader->fromString($address);
512 1
        if ($decoded instanceof CashAddress) {
513
            $address = $decoded->getLegacyAddress();
514
        }
515
516 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
0 ignored issues
show
Bug introduced by
It seems like $address defined by $decoded->getLegacyAddress() on line 513 can also be of type object<BitWasp\Bitcoin\A...PayToPubKeyHashAddress> or object<BitWasp\Bitcoin\Address\ScriptHashAddress>; however, Blocktrail\SDK\Blocktrai...ce::getPathForAddress() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
517
    }
518
519
    /**
520
     * @param string|BIP32Path  $path
521
     * @return BIP32Key
522
     * @throws \Exception
523
     */
524 18
    public function getBlocktrailPublicKey($path) {
525 18
        $path = BIP32Path::path($path);
526
527 18
        $keyIndex = str_replace("'", "", $path[1]);
528
529 18
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
530
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
531
        }
532
533 18
        return $this->blocktrailPublicKeys[$keyIndex];
534
    }
535
536
    /**
537
     * generate a new derived key and return the new path and address for it
538
     *
539
     * @param int|null $chainIndex
540
     * @return string[]     [path, address]
541
     */
542 17
    public function getNewAddressPair($chainIndex = null) {
543 17
        $path = $this->getNewDerivation($chainIndex);
544 16
        $address = $this->getAddressByPath($path);
545
546 16
        return [$path, $address];
547
    }
548
549
    /**
550
     * generate a new derived private key and return the new address for it
551
     *
552
     * @param int|null $chainIndex
553
     * @return string
554
     */
555 9
    public function getNewAddress($chainIndex = null) {
556 9
        return $this->getNewAddressPair($chainIndex)[1];
557
    }
558
559
    /**
560
     * generate a new derived private key and return the new address for it
561
     *
562
     * @return string
563
     */
564 5
    public function getNewChangeAddress() {
565 5
        return $this->getNewAddressPair($this->changeIndex)[1];
566
    }
567
568
    /**
569
     * get the balance for the wallet
570
     *
571
     * @return int[]            [confirmed, unconfirmed]
572
     */
573 9
    public function getBalance() {
574 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
575
576 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
577
    }
578
579
    /**
580
     * do wallet discovery (slow)
581
     *
582
     * @param int   $gap        the gap setting to use for discovery
583
     * @return int[]            [confirmed, unconfirmed]
584
     */
585 2
    public function doDiscovery($gap = 200) {
586 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
587
588 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
589
    }
590
591
    /**
592
     * create, sign and send a transaction
593
     *
594
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
595
     *                                      value should be INT
596
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
597
     * @param bool     $allowZeroConf
598
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
599
     * @param string   $feeStrategy
600
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
601
     * @return string the txid / transaction hash
602
     * @throws \Exception
603
     */
604 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
605 9
        if ($this->locked) {
606 4
            throw new \Exception("Wallet needs to be unlocked to pay");
607
        }
608
609 9
        if ($forceFee && $feeStrategy !== self::FEE_STRATEGY_FORCE_FEE) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $forceFee of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
610
            throw new \InvalidArgumentException("feeStrategy should be set to force_fee to set a forced fee");
611
        }
612
613 9
        $outputs = (new OutputsNormalizer($this->getAddressReader()))->normalize($outputs);
614
615 9
        $txBuilder = new TransactionBuilder($this->addressReader);
616 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
617 9
        $txBuilder->setFeeStrategy($feeStrategy);
618 9
        $txBuilder->setChangeAddress($changeAddress);
619
620 9
        foreach ($outputs as $output) {
621 9
            $txBuilder->addOutput($output);
622
        }
623
624 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
625
626 3
        $apiCheckFee = $forceFee === null;
627
628 3
        return $this->sendTx($txBuilder, $apiCheckFee);
629
    }
630
631
    /**
632
     * determine max spendable from wallet after fees
633
     *
634
     * @param bool     $allowZeroConf
635
     * @param string   $feeStrategy
636
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
637
     * @param int      $outputCnt
638
     * @return string
639
     * @throws BlocktrailSDKException
640
     */
641
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
642
        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...
643
    }
644
645
    /**
646
     * parse outputs into normalized struct
647
     *
648
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
649
     * @return array            [['address' => address, 'value' => value], ]
650
     */
651 1
    public static function normalizeOutputsStruct(array $outputs) {
652 1
        $result = [];
653
654 1
        foreach ($outputs as $k => $v) {
655 1
            if (is_numeric($k)) {
656 1
                if (!is_array($v)) {
657
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
658
                }
659
660 1
                if (isset($v['address']) && isset($v['value'])) {
661 1
                    $address = $v['address'];
662 1
                    $value = $v['value'];
663 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
664 1
                    $address = $v[0];
665 1
                    $value = $v[1];
666
                } else {
667 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
668
                }
669
            } else {
670 1
                $address = $k;
671 1
                $value = $v;
672
            }
673
674 1
            $result[] = ['address' => $address, 'value' => $value];
675
        }
676
677 1
        return $result;
678
    }
679
680
    /**
681
     * 'fund' the txBuilder with UTXOs (modified in place)
682
     *
683
     * @param TransactionBuilder    $txBuilder
684
     * @param bool|true             $lockUTXOs
685
     * @param bool|false            $allowZeroConf
686
     * @param null|int              $forceFee
687
     * @return TransactionBuilder
688
     */
689 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
690
691
        // get the data we should use for this transaction
692 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 689 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...
693
        
694 5
        $utxos = $coinSelection['utxos'];
695 5
        $fee = $coinSelection['fee'];
696 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...
697
698 5
        if ($forceFee !== null) {
699 1
            $txBuilder->setFee($forceFee);
700
        } else {
701 5
            $txBuilder->validateFee($fee);
702
        }
703
704 5
        foreach ($utxos as $utxo) {
705 5
            $signMode = SignInfo::MODE_SIGN;
706 5
            if (isset($utxo['sign_mode'])) {
707
                $signMode = $utxo['sign_mode'];
708
                if (!in_array($signMode, $this->allowedSignModes)) {
709
                    throw new \Exception("Sign mode disallowed by wallet");
710
                }
711
            }
712
713 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
714
        }
715
716 5
        return $txBuilder;
717
    }
718
719
    /**
720
     * build inputs and outputs lists for TransactionBuilder
721
     *
722
     * @param TransactionBuilder $txBuilder
723
     * @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...
724
     * @throws \Exception
725
     */
726 7
    public function buildTx(TransactionBuilder $txBuilder) {
727 7
        $send = $txBuilder->getOutputs();
728 7
        $utxos = $txBuilder->getUtxos();
729 7
        $signInfo = [];
730
731 7
        $txb = new TxBuilder();
732
733 7
        foreach ($utxos as $utxo) {
734 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
735 1
                $tx = $this->sdk->transaction($utxo->hash);
736
737 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...
738
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
739
                }
740
741 1
                $output = $tx['outputs'][$utxo->index];
742
743 1
                if (!$utxo->address) {
744
                    $utxo->address = $this->addressReader->fromString($output['address']);
745
                }
746 1
                if (!$utxo->value) {
747
                    $utxo->value = $output['value'];
748
                }
749 1
                if (!$utxo->scriptPubKey) {
750 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
751
                }
752
            }
753
754 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
755 7
                if (!$utxo->path) {
756
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
757
                }
758
759 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
760 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...
761 6
                    $utxo->redeemScript = $redeemScript;
762 6
                    $utxo->witnessScript = $witnessScript;
763
                }
764
            }
765
766 7
            $signInfo[] = $utxo->getSignInfo();
767
        }
768
769
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
770 7
            return $utxo->value;
771 7
        }, $utxos));
772 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
773 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
774
        }
775
776 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getHighPriorityFeePerKB(), $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
777
778 7
        if ($txBuilder->getValidateFee() !== null) {
779
            // sanity check to make sure the API isn't giving us crappy data
780 5
            if (abs($txBuilder->getValidateFee() - $fee) > (Wallet::BASE_FEE * 5)) {
781
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
782
            }
783
        }
784
785 7
        if ($change > 0) {
786 6
            $send[] = [
787 6
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewChangeAddress(),
788 6
                'value' => $change
789
            ];
790
        }
791
792 7
        foreach ($utxos as $utxo) {
793 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
794
        }
795
796
        // outputs should be randomized to make the change harder to detect
797 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
798 7
            shuffle($send);
799
        }
800
801 7
        foreach ($send as $out) {
802 7
            assert(isset($out['value']));
803
804 7
            if (isset($out['scriptPubKey'])) {
805 7
                $txb->output($out['value'], $out['scriptPubKey']);
806 6
            } elseif (isset($out['address'])) {
807 6
                $txb->output($out['value'], $this->addressReader->fromString($out['address'])->getScriptPubKey());
808
            } else {
809 7
                throw new \Exception();
810
            }
811
        }
812
813 7
        return [$txb->get(), $signInfo];
814
    }
815
816 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
817 7
        $send = (new OutputsNormalizer($this->addressReader))->normalize($txBuilder->getOutputs());
818 7
        $utxos = $txBuilder->getUtxos();
819
820 7
        $fee = $txBuilder->getFee();
821 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...
822
823
        // if the fee is fixed we just need to calculate the change
824 7
        if ($fee !== null) {
825 2
            $change = $this->determineChange($utxos, $send, $fee);
826
827
            // if change is not dust we need to add a change output
828 2
            if ($change > Blocktrail::DUST) {
829 1
                $send[] = ['address' => 'change', 'value' => $change];
830
            } else {
831
                // if change is dust we add it to the fee
832 1
                $fee += $change;
833 1
                $change = 0;
834
            }
835
836 2
            return [$fee, $change];
837
        } else {
838 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
839
840 7
            $change = $this->determineChange($utxos, $send, $fee);
841
842 7
            if ($change > 0) {
843 6
                $changeIdx = count($send);
844
                // set dummy change output
845 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
846
847
                // recaculate fee now that we know that we have a change output
848 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
849
850
                // unset dummy change output
851 6
                unset($send[$changeIdx]);
852
853
                // if adding the change output made the fee bump up and the change is smaller than the fee
854
                //  then we're not doing change
855 6
                if ($fee2 > $fee && $fee2 > $change) {
856
                    $change = 0;
857
                } else {
858 6
                    $change = $this->determineChange($utxos, $send, $fee2);
859
860
                    // if change is not dust we need to add a change output
861 6
                    if ($change > Blocktrail::DUST) {
862 6
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
863
                    } else {
864
                        // if change is dust we do nothing (implicitly it's added to the fee)
865 1
                        $change = 0;
866
                    }
867
                }
868
            }
869
870
871 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
872
873 7
            return [$fee, $change];
874
        }
875
    }
876
877
    /**
878
     * create, sign and send transction based on TransactionBuilder
879
     *
880
     * @param TransactionBuilder $txBuilder
881
     * @param bool $apiCheckFee     let the API check if the fee is correct
882
     * @return string
883
     * @throws \Exception
884
     */
885 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
886 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
887
888 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...
889
    }
890
891
    /**
892
     * !! INTERNAL METHOD, public for testing purposes !!
893
     * create, sign and send transction based on inputs and outputs
894
     *
895
     * @param Transaction $tx
896
     * @param SignInfo[]  $signInfo
897
     * @param bool $apiCheckFee     let the API check if the fee is correct
898
     * @return string
899
     * @throws \Exception
900
     * @internal
901
     */
902 4
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
903 4
        if ($this->locked) {
904
            throw new \Exception("Wallet needs to be unlocked to pay");
905
        }
906
907
        assert(Util::all(function ($signInfo) {
908 4
            return $signInfo instanceof SignInfo;
909 4
        }, $signInfo), '$signInfo should be SignInfo[]');
910
911
        // sign the transaction with our keys
912 4
        $signed = $this->signTransaction($tx, $signInfo);
913
914
        $txs = [
915 4
            'signed_transaction' => $signed->getHex(),
916 4
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
917
        ];
918
919
        // send the transaction
920
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
921 4
            return (string)$r->path;
922 4
        }, $signInfo), $apiCheckFee);
923
    }
924
925
    /**
926
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
927
     *
928
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
929
     * @param int $utxoCnt      number of unspent inputs in transaction
930
     * @param int $outputCnt    number of outputs in transaction
931
     * @return float
932
     * @access public           reminder that people might use this!
933
     */
934 1
    public static function estimateFee($utxoCnt, $outputCnt) {
935 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
936
937 1
        return self::baseFeeForSize($size);
938
    }
939
940
    /**
941
     * @param int $size     size in bytes
942
     * @return int          fee in satoshi
943
     */
944 5
    public static function baseFeeForSize($size) {
945 5
        $sizeKB = (int)ceil($size / 1000);
946
947 5
        return $sizeKB * self::BASE_FEE;
948
    }
949
950
    /**
951
     * @todo: variable varint
952
     * @todo: deprecate
953
     * @param int $txinSize
954
     * @param int $txoutSize
955
     * @return float
956
     */
957 2
    public static function estimateSize($txinSize, $txoutSize) {
958 2
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
959
    }
960
961
    /**
962
     * only supports estimating size for P2PKH/P2SH outputs
963
     *
964
     * @param int $outputCnt    number of outputs in transaction
965
     * @return float
966
     */
967 2
    public static function estimateSizeOutputs($outputCnt) {
968 2
        return ($outputCnt * 34);
969
    }
970
971
    /**
972
     * only supports estimating size for 2of3 multsig UTXOs
973
     *
974
     * @param int $utxoCnt      number of unspent inputs in transaction
975
     * @return float
976
     */
977 3
    public static function estimateSizeUTXOs($utxoCnt) {
978 3
        $txinSize = 0;
979
980 3
        for ($i=0; $i<$utxoCnt; $i++) {
981
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
982 3
            $multisig = "2of3";
983
984 3
            if ($multisig) {
985 3
                $sigCnt = 2;
986 3
                $msig = explode("of", $multisig);
987 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
988 3
                    $sigCnt = $msig[0];
989
                }
990
991 3
                $txinSize += array_sum([
992 3
                    32, // txhash
993 3
                    4, // idx
994 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...
995 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...
996
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
997
                    1, // OP_0
998 3
                    4, // sequence
999
                ]);
1000
            } else {
1001
                $txinSize += array_sum([
1002
                    32, // txhash
1003
                    4, // idx
1004
                    73, // sig
1005
                    34, // script
1006
                    4, // sequence
1007
                ]);
1008
            }
1009
        }
1010
1011 3
        return $txinSize;
1012
    }
1013
1014
    /**
1015
     * determine how much fee is required based on the inputs and outputs
1016
     *  this is an estimation, not a proper 100% correct calculation
1017
     *
1018
     * @param UTXO[]  $utxos
1019
     * @param array[] $outputs
1020
     * @param         $feeStrategy
1021
     * @param         $highPriorityFeePerKB
1022
     * @param         $optimalFeePerKB
1023
     * @param         $lowPriorityFeePerKB
1024
     * @return int
1025
     * @throws BlocktrailSDKException
1026
     */
1027 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
1028
1029 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
1030
1031
        switch ($feeStrategy) {
1032 7
            case self::FEE_STRATEGY_BASE_FEE:
1033 4
                return self::baseFeeForSize($size);
1034
1035 4
            case self::FEE_STRATEGY_HIGH_PRIORITY:
1036
                return (int)round(($size / 1000) * $highPriorityFeePerKB);
1037
1038 4
            case self::FEE_STRATEGY_OPTIMAL:
1039 4
                return (int)round(($size / 1000) * $optimalFeePerKB);
1040
1041 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
1042 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
1043
1044
            case self::FEE_STRATEGY_FORCE_FEE:
1045
                throw new BlocktrailSDKException("Can't determine when for force_fee");
1046
1047
            default:
1048
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
1049
        }
1050
    }
1051
1052
    /**
1053
     * determine how much change is left over based on the inputs and outputs and the fee
1054
     *
1055
     * @param UTXO[]    $utxos
1056
     * @param array[]   $outputs
1057
     * @param int       $fee
1058
     * @return int
1059
     */
1060 7
    protected function determineChange($utxos, $outputs, $fee) {
1061
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
1062 7
            return $utxo->value;
1063 7
        }, $utxos));
1064 7
        $outputsTotal = array_sum(array_column($outputs, 'value'));
1065
1066 7
        return $inputsTotal - $outputsTotal - $fee;
1067
    }
1068
1069
    /**
1070
     * sign a raw transaction with the private keys that we have
1071
     *
1072
     * @param Transaction $tx
1073
     * @param SignInfo[]  $signInfo
1074
     * @return TransactionInterface
1075
     * @throws \Exception
1076
     */
1077 4
    protected function signTransaction(Transaction $tx, array $signInfo) {
1078 4
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1079
1080 4
        assert(Util::all(function ($signInfo) {
1081 4
            return $signInfo instanceof SignInfo;
1082 4
        }, $signInfo), '$signInfo should be SignInfo[]');
1083
1084 4
        $sigHash = SigHash::ALL;
1085 4
        if ($this->network === "bitcoincash") {
1086
            $sigHash |= SigHash::BITCOINCASH;
1087
            $signer->redeemBitcoinCash(true);
1088
        }
1089
1090 4
        foreach ($signInfo as $idx => $info) {
1091 4
            if ($info->mode === SignInfo::MODE_SIGN) {
1092
                // required SignInfo: path, redeemScript|witnessScript, output
1093 4
                $path = BIP32Path::path($info->path)->privatePath();
1094 4
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1095 4
                $signData = new SignData();
1096 4
                if ($info->redeemScript) {
1097 4
                    $signData->p2sh($info->redeemScript);
1098
                }
1099 4
                if ($info->witnessScript) {
1100 1
                    $signData->p2wsh($info->witnessScript);
1101
                }
1102 4
                $input = $signer->input($idx, $info->output, $signData);
1103 4
                $input->sign($key, $sigHash);
1104
            }
1105
        }
1106
1107 4
        return $signer->get();
1108
    }
1109
1110
    /**
1111
     * send the transaction using the API
1112
     *
1113
     * @param string|array  $signed
1114
     * @param string[]      $paths
1115
     * @param bool          $checkFee
1116
     * @return string           the complete raw transaction
1117
     * @throws \Exception
1118
     */
1119 4
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1120 4
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1121
    }
1122
1123
    /**
1124
     * @param \array[] $outputs
1125
     * @param bool $lockUTXO
1126
     * @param bool $allowZeroConf
1127
     * @param int|null|string $feeStrategy
1128
     * @param null $forceFee
1129
     * @return array
1130
     */
1131 12
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1132 12
        $send = [];
1133 12
        foreach ((new OutputsNormalizer($this->addressReader))->normalize($outputs) as $output) {
1134 12
            $send[] = [
1135 12
                "value" => $output['value'],
1136 12
                "scriptPubKey" => $output['scriptPubKey']->getHex(),
1137
            ];
1138
        }
1139
1140 12
        $result = $this->sdk->coinSelection($this->identifier, $send, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1141
1142 6
        $this->highPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_HIGH_PRIORITY];
1143 6
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1144 6
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1145 6
        $this->feePerKBAge = time();
1146
1147 6
        return $result;
1148
    }
1149
1150 7
    public function getHighPriorityFeePerKB() {
1151 7
        if (!$this->highPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1152 3
            $this->updateFeePerKB();
1153
        }
1154
1155 7
        return $this->highPriorityFeePerKB;
1156
    }
1157
1158 7
    public function getOptimalFeePerKB() {
1159 7
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1160
            $this->updateFeePerKB();
1161
        }
1162
1163 7
        return $this->optimalFeePerKB;
1164
    }
1165
1166 7
    public function getLowPriorityFeePerKB() {
1167 7
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1168
            $this->updateFeePerKB();
1169
        }
1170
1171 7
        return $this->lowPriorityFeePerKB;
1172
    }
1173
1174 3
    public function updateFeePerKB() {
1175 3
        $result = $this->sdk->feePerKB();
1176
1177 3
        $this->highPriorityFeePerKB = $result[self::FEE_STRATEGY_HIGH_PRIORITY];
1178 3
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1179 3
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1180
1181 3
        $this->feePerKBAge = time();
1182 3
    }
1183
1184
    /**
1185
     * delete the wallet
1186
     *
1187
     * @param bool $force ignore warnings (such as non-zero balance)
1188
     * @return mixed
1189
     * @throws \Exception
1190
     */
1191 10
    public function deleteWallet($force = false) {
1192 10
        if ($this->locked) {
1193
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1194
        }
1195
1196 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1197 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1198
    }
1199
1200
    /**
1201
     * create checksum to verify ownership of the master primary key
1202
     *
1203
     * @return string[]     [address, signature]
1204
     */
1205 10
    protected function createChecksumVerificationSignature() {
1206 10
        $privKey = $this->primaryPrivateKey->key();
1207
1208 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1209 10
        $address = $pubKey->getAddress()->getAddress();
1210
1211 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1212 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1213
1214 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1215
    }
1216
1217
    /**
1218
     * setup a webhook for our wallet
1219
     *
1220
     * @param string    $url            URL to receive webhook events
1221
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1222
     * @return array
1223
     */
1224 1
    public function setupWebhook($url, $identifier = null) {
1225 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1226 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1227
    }
1228
1229
    /**
1230
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1231
     * @return mixed
1232
     */
1233 1
    public function deleteWebhook($identifier = null) {
1234 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1235 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1236
    }
1237
1238
    /**
1239
     * lock a specific unspent output
1240
     *
1241
     * @param     $txHash
1242
     * @param     $txIdx
1243
     * @param int $ttl
1244
     * @return bool
1245
     */
1246
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1247
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1248
    }
1249
1250
    /**
1251
     * unlock a specific unspent output
1252
     *
1253
     * @param     $txHash
1254
     * @param     $txIdx
1255
     * @return bool
1256
     */
1257
    public function unlockUTXO($txHash, $txIdx) {
1258
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1259
    }
1260
1261
    /**
1262
     * get all transactions for the wallet (paginated)
1263
     *
1264
     * @param  integer $page    pagination: page number
1265
     * @param  integer $limit   pagination: records per page (max 500)
1266
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1267
     * @return array            associative array containing the response
1268
     */
1269 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1270 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1271
    }
1272
1273
    /**
1274
     * get all addresses for the wallet (paginated)
1275
     *
1276
     * @param  integer $page    pagination: page number
1277
     * @param  integer $limit   pagination: records per page (max 500)
1278
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1279
     * @return array            associative array containing the response
1280
     */
1281 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1282 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1283
    }
1284
1285
    /**
1286
     * get all UTXOs for the wallet (paginated)
1287
     *
1288
     * @param  integer $page        pagination: page number
1289
     * @param  integer $limit       pagination: records per page (max 500)
1290
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1291
     * @param  boolean $zeroconf    include zero confirmation transactions
1292
     * @return array                associative array containing the response
1293
     */
1294 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1295 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1296
    }
1297
}
1298