Completed
Branch master (211d82)
by
unknown
03:00
created

Wallet   F

Complexity

Total Complexity 150

Size/Duplication

Total Lines 1207
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 34

Test Coverage

Coverage 89.36%

Importance

Changes 0
Metric Value
dl 0
loc 1207
ccs 361
cts 404
cp 0.8936
rs 0.5217
c 0
b 0
f 0
wmc 150
lcom 1
cbo 34

53 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 33 5
A getWalletPath() 0 10 3
A isSegwit() 0 3 1
A getIdentifier() 0 3 1
A getBackupKey() 0 3 1
A getBlocktrailPublicKeys() 0 5 1
A isLocked() 0 3 1
B upgradeKeyIndex() 0 27 4
C getNewDerivation() 0 32 8
A getParentPublicKey() 0 17 4
A getAddressByPath() 0 11 2
A getWalletScriptByPath() 0 12 2
A getRedeemScriptByPath() 0 7 3
A getAddressFromKey() 0 3 1
B getWalletScriptFromKey() 0 26 5
A getPathForAddress() 0 3 1
A getBlocktrailPublicKey() 0 11 2
A getNewAddressPair() 0 6 1
A getNewAddress() 0 3 1
A getNewChangeAddress() 0 3 1
A getBalance() 0 5 1
A doDiscovery() 0 5 1
B pay() 0 26 5
A getMaxSpendable() 0 3 1
D normalizeOutputsStruct() 0 28 9
B coinSelectionForTxBuilder() 0 28 5
F buildTx() 0 89 24
B determineFeeAndChange() 0 60 7
A sendTx() 0 5 1
A _sendTx() 0 22 2
A estimateFee() 0 5 1
A baseFeeForSize() 0 5 1
A estimateSize() 0 3 1
A estimateSizeOutputs() 0 3 1
B estimateSizeUTXOs() 0 36 5
B determineFee() 0 24 6
A determineChange() 0 8 1
B signTransaction() 0 32 6
A sendTransaction() 0 3 1
A coinSelection() 0 10 1
A getHighPriorityFeePerKB() 0 7 3
A getOptimalFeePerKB() 0 7 3
A getLowPriorityFeePerKB() 0 7 3
A updateFeePerKB() 0 9 1
A deleteWallet() 0 8 2
A createChecksumVerificationSignature() 0 11 1
A setupWebhook() 0 4 2
A deleteWebhook() 0 4 2
A lockUTXO() 0 3 1
A unlockUTXO() 0 3 1
A transactions() 0 3 1
A addresses() 0 3 1
A utxos() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Wallet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Wallet, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
8
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
9
use BitWasp\Bitcoin\Script\P2shScript;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\WitnessScript;
13
use BitWasp\Bitcoin\Transaction\Factory\SignData;
14
use BitWasp\Bitcoin\Transaction\Factory\Signer;
15
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
16
use BitWasp\Bitcoin\Transaction\OutPoint;
17
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
18
use BitWasp\Bitcoin\Transaction\Transaction;
19
use BitWasp\Bitcoin\Transaction\TransactionInterface;
20
use BitWasp\Buffertools\Buffer;
21
use Blocktrail\SDK\Bitcoin\BIP32Key;
22
use Blocktrail\SDK\Bitcoin\BIP32Path;
23
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
24
25
/**
26
 * Class Wallet
27
 */
28
abstract class Wallet implements WalletInterface {
29
30
    const WALLET_VERSION_V1 = 'v1';
31
    const WALLET_VERSION_V2 = 'v2';
32
    const WALLET_VERSION_V3 = 'v3';
33
34
    const CHAIN_BTC_DEFAULT = 0;
35
    const CHAIN_BCC_DEFAULT = 1;
36
    const CHAIN_BTC_SEGWIT = 2;
37
38
    const BASE_FEE = 10000;
39
40
    /**
41
     * development / debug setting
42
     *  when getting a new derivation from the API,
43
     *  will verify address / redeeemScript with the values the API provides
44
     */
45
    const VERIFY_NEW_DERIVATION = true;
46
47
    /**
48
     * @var BlocktrailSDKInterface
49
     */
50
    protected $sdk;
51
52
    /**
53
     * @var string
54
     */
55
    protected $identifier;
56
57
    /**
58
     * BIP32 master primary private key (m/)
59
     *
60
     * @var BIP32Key
61
     */
62
    protected $primaryPrivateKey;
63
64
    /**
65
     * @var BIP32Key[]
66
     */
67
    protected $primaryPublicKeys;
68
69
    /**
70
     * BIP32 master backup public key (M/)
71
72
     * @var BIP32Key
73
     */
74
    protected $backupPublicKey;
75
76
    /**
77
     * map of blocktrail BIP32 public keys
78
     *  keyed by key index
79
     *  path should be `M / key_index'`
80
     *
81
     * @var BIP32Key[]
82
     */
83
    protected $blocktrailPublicKeys;
84
85
    /**
86
     * the 'Blocktrail Key Index' that is used for new addresses
87
     *
88
     * @var int
89
     */
90
    protected $keyIndex;
91
92
    /**
93
     * 'bitcoin'
94
     *
95
     * @var string
96
     */
97
    protected $network;
98
99
    /**
100
     * testnet yes / no
101
     *
102
     * @var bool
103
     */
104
    protected $testnet;
105
106
    /**
107
     * cache of public keys, by path
108
     *
109
     * @var BIP32Key[]
110
     */
111
    protected $pubKeys = [];
112
113
    /**
114
     * cache of address / redeemScript, by path
115
     *
116
     * @var string[][]      [[address, redeemScript)], ]
117
     */
118
    protected $derivations = [];
119
120
    /**
121
     * reverse cache of paths by address
122
     *
123
     * @var string[]
124
     */
125
    protected $derivationsByAddress = [];
126
127
    protected $checksum;
128
129
    /**
130
     * @var bool
131
     */
132
    protected $locked = true;
133
134
    /**
135
     * @var bool
136
     */
137
    protected $isSegwit = false;
138
139
    /**
140
     * @var int
141
     */
142
    protected $chainIndex;
143
144
    /**
145
     * @var int
146
     */
147
    protected $changeIndex;
148
149
    protected $highPriorityFeePerKB;
150
    protected $optimalFeePerKB;
151
    protected $lowPriorityFeePerKB;
152
    protected $feePerKBAge;
153
    protected $allowedSignModes = [SignInfo::MODE_DONTSIGN, SignInfo::MODE_SIGN];
154
155
    /**
156
     * @param BlocktrailSDKInterface        $sdk                        SDK instance used to do requests
157
     * @param string                        $identifier                 identifier of the wallet
158
     * @param BIP32Key[]                    $primaryPublicKeys
159
     * @param BIP32Key                      $backupPublicKey            should be BIP32 master public key M/
160
     * @param BIP32Key[]                    $blocktrailPublicKeys
161
     * @param int                           $keyIndex
162
     * @param string                        $network
163
     * @param bool                          $testnet
164
     * @param bool                          $segwit
165
     * @param string                        $checksum
166
     * @throws BlocktrailSDKException
167
     */
168 22
    public function __construct(BlocktrailSDKInterface $sdk, $identifier, array $primaryPublicKeys, $backupPublicKey, array $blocktrailPublicKeys, $keyIndex, $network, $testnet, $segwit, $checksum) {
169 22
        $this->sdk = $sdk;
170
171 22
        $this->identifier = $identifier;
172 22
        $this->backupPublicKey = BlocktrailSDK::normalizeBIP32Key($backupPublicKey);
173 22
        $this->primaryPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($primaryPublicKeys);
174 22
        $this->blocktrailPublicKeys = BlocktrailSDK::normalizeBIP32KeyArray($blocktrailPublicKeys);
175
176 22
        $this->network = $network;
177 22
        $this->testnet = $testnet;
178 22
        $this->keyIndex = $keyIndex;
179 22
        $this->checksum = $checksum;
180
181 22
        if ($network === "bitcoin") {
182 22
            if ($segwit) {
183 3
                $chainIdx = self::CHAIN_BTC_DEFAULT;
184 3
                $changeIdx = self::CHAIN_BTC_SEGWIT;
185
            } else {
186 20
                $chainIdx = self::CHAIN_BTC_DEFAULT;
187 22
                $changeIdx = self::CHAIN_BTC_DEFAULT;
188
            }
189
        } else {
190
            if ($segwit && $network === "bitcoincash") {
191
                throw new BlocktrailSDKException("Received segwit flag for bitcoincash - abort");
192
            }
193
            $chainIdx = self::CHAIN_BCC_DEFAULT;
194
            $changeIdx = self::CHAIN_BCC_DEFAULT;
195
        }
196
197 22
        $this->isSegwit = (bool) $segwit;
198 22
        $this->chainIndex = $chainIdx;
199 22
        $this->changeIndex = $changeIdx;
200 22
    }
201
202
    /**
203
     * @param int|null $chainIndex
204
     * @return WalletPath
205
     * @throws BlocktrailSDKException
206
     */
207 14
    protected function getWalletPath($chainIndex = null) {
208 14
        if ($chainIndex === null) {
209 12
            return WalletPath::create($this->keyIndex, $this->chainIndex);
210
        } else {
211 6
            if (!is_int($chainIndex)) {
212 1
                throw new BlocktrailSDKException("Chain index is invalid - should be an integer");
213
            }
214 5
            return WalletPath::create($this->keyIndex, $chainIndex);
215
        }
216
    }
217
218
    /**
219
     * @return bool
220
     */
221 3
    public function isSegwit() {
222 3
        return $this->isSegwit;
223
    }
224
225
    /**
226
     * return the wallet identifier
227
     *
228
     * @return string
229
     */
230 10
    public function getIdentifier() {
231 10
        return $this->identifier;
232
    }
233
234
    /**
235
     * Returns the wallets backup public key
236
     *
237
     * @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...
238
     */
239 1
    public function getBackupKey() {
240 1
        return $this->backupPublicKey->tuple();
241
    }
242
243
    /**
244
     * return list of Blocktrail co-sign extended public keys
245
     *
246
     * @return array[]      [ [xpub, path] ]
247
     */
248 5
    public function getBlocktrailPublicKeys() {
249
        return array_map(function (BIP32Key $key) {
250 5
            return $key->tuple();
251 5
        }, $this->blocktrailPublicKeys);
252
    }
253
254
    /**
255
     * check if wallet is locked
256
     *
257
     * @return bool
258
     */
259 10
    public function isLocked() {
260 10
        return $this->locked;
261
    }
262
263
    /**
264
     * upgrade wallet to different blocktrail cosign key
265
     *
266
     * @param $keyIndex
267
     * @return bool
268
     * @throws \Exception
269
     */
270 5
    public function upgradeKeyIndex($keyIndex) {
271 5
        if ($this->locked) {
272 4
            throw new \Exception("Wallet needs to be unlocked to upgrade key index");
273
        }
274
275 5
        $walletPath = WalletPath::create($keyIndex);
276
277
        // do the upgrade to the new 'key_index'
278 5
        $primaryPublicKey = $this->primaryPrivateKey->buildKey((string)$walletPath->keyIndexPath()->publicPath());
279
280
        // $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...
281 5
        $result = $this->sdk->upgradeKeyIndex($this->identifier, $keyIndex, $primaryPublicKey->tuple());
282
283 5
        $this->primaryPublicKeys[$keyIndex] = $primaryPublicKey;
284 5
        $this->keyIndex = $keyIndex;
285
286
        // update the blocktrail public keys
287 5
        foreach ($result['blocktrail_public_keys'] as $keyIndex => $pubKey) {
288 5
            if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
289 5
                $path = $pubKey[1];
290 5
                $pubKey = $pubKey[0];
291 5
                $this->blocktrailPublicKeys[$keyIndex] = BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKey), $path);
292
            }
293
        }
294
295 5
        return true;
296
    }
297
298
    /**
299
     * get a new BIP32 derivation for the next (unused) address
300
     *  by requesting it from the API
301
     *
302
     * @return string
303
     * @param int|null $chainIndex
304
     * @throws \Exception
305
     */
306 14
    protected function getNewDerivation($chainIndex = null) {
307 14
        $path = $this->getWalletPath($chainIndex)->path()->last("*");
308
309 13
        if (self::VERIFY_NEW_DERIVATION) {
310 13
            $new = $this->sdk->_getNewDerivation($this->identifier, (string)$path);
311
312 13
            $path = $new['path'];
313 13
            $address = $new['address'];
314 13
            $redeemScript = $new['redeem_script'];
315 13
            $witnessScript = array_key_exists('witness_script', $new) ? $new['witness_script'] : null;
316
317
            /** @var ScriptInterface $checkRedeemScript */
318
            /** @var ScriptInterface $checkWitnessScript */
319 13
            list($checkAddress, $checkRedeemScript, $checkWitnessScript) = $this->getRedeemScriptByPath($path);
320
321 13
            if ($checkAddress != $address) {
322
                throw new \Exception("Failed to verify that address from API [{$address}] matches address locally [{$checkAddress}]");
323
            }
324
325 13
            if ($checkRedeemScript && $checkRedeemScript->getHex() != $redeemScript) {
326
                throw new \Exception("Failed to verify that redeemScript from API [{$redeemScript}] matches address locally [{$checkRedeemScript->getHex()}]");
327
            }
328
329 13
            if ($checkWitnessScript && $checkWitnessScript->getHex() != $witnessScript) {
330 13
                throw new \Exception("Failed to verify that witnessScript from API [{$witnessScript}] matches address locally [{$checkWitnessScript->getHex()}]");
331
            }
332
        } else {
333
            $path = $this->sdk->getNewDerivation($this->identifier, (string)$path);
334
        }
335
336 13
        return (string)$path;
337
    }
338
339
    /**
340
     * @param string|BIP32Path  $path
341
     * @return BIP32Key|false
342
     * @throws \Exception
343
     *
344
     * @TODO: hmm?
345
     */
346 16
    protected function getParentPublicKey($path) {
347 16
        $path = BIP32Path::path($path)->parent()->publicPath();
348
349 16
        if ($path->count() <= 2) {
350
            return false;
351
        }
352
353 16
        if ($path->isHardened()) {
354
            return false;
355
        }
356
357 16
        if (!isset($this->pubKeys[(string)$path])) {
358 16
            $this->pubKeys[(string)$path] = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
359
        }
360
361 16
        return $this->pubKeys[(string)$path];
362
    }
363
364
    /**
365
     * get address for the specified path
366
     *
367
     * @param string|BIP32Path  $path
368
     * @return string
369
     */
370 13
    public function getAddressByPath($path) {
371 13
        $path = (string)BIP32Path::path($path)->privatePath();
372 13
        if (!isset($this->derivations[$path])) {
373 13
            list($address, ) = $this->getRedeemScriptByPath($path);
374
375 13
            $this->derivations[$path] = $address;
376 13
            $this->derivationsByAddress[$address] = $path;
377
        }
378
379 13
        return $this->derivations[$path];
380
    }
381
382
    /**
383
     * @param string $path
384
     * @return WalletScript
385
     */
386 16
    public function getWalletScriptByPath($path) {
387 16
        $path = BIP32Path::path($path);
388
389
        // optimization to avoid doing BitcoinLib::private_key_to_public_key too much
390 16
        if ($pubKey = $this->getParentPublicKey($path)) {
391 16
            $key = $pubKey->buildKey($path->publicPath());
392
        } else {
393
            $key = $this->primaryPublicKeys[$path->getKeyIndex()]->buildKey($path);
394
        }
395
396 16
        return $this->getWalletScriptFromKey($key, $path);
397
    }
398
399
    /**
400
     * get address and redeemScript for specified path
401
     *
402
     * @param string    $path
403
     * @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...
404
     */
405 15
    public function getRedeemScriptByPath($path) {
406 15
        $walletScript = $this->getWalletScriptByPath($path);
407
408 15
        $redeemScript = $walletScript->isP2SH() ? $walletScript->getRedeemScript() : null;
409 15
        $witnessScript = $walletScript->isP2WSH() ? $walletScript->getWitnessScript() : null;
410 15
        return [$walletScript->getAddress()->getAddress(), $redeemScript, $witnessScript];
411
    }
412
413
    /**
414
     * @param BIP32Key          $key
415
     * @param string|BIP32Path  $path
416
     * @return string
417
     */
418
    protected function getAddressFromKey(BIP32Key $key, $path) {
419
        return $this->getWalletScriptFromKey($key, $path)->getAddress()->getAddress();
420
    }
421
422
    /**
423
     * @param BIP32Key          $key
424
     * @param string|BIP32Path  $path
425
     * @return WalletScript
426
     * @throws \Exception
427
     */
428 16
    protected function getWalletScriptFromKey(BIP32Key $key, $path) {
429 16
        $path = BIP32Path::path($path)->publicPath();
430
431 16
        $blocktrailPublicKey = $this->getBlocktrailPublicKey($path);
432
433 16
        $multisig = ScriptFactory::scriptPubKey()->multisig(2, BlocktrailSDK::sortMultisigKeys([
434 16
            $key->buildKey($path)->publicKey(),
435 16
            $this->backupPublicKey->buildKey($path->unhardenedPath())->publicKey(),
436 16
            $blocktrailPublicKey->buildKey($path)->publicKey()
437 16
        ]), false);
438
439 16
        $type = (int)$key->path()[2];
440 16
        if ($this->isSegwit && $type === Wallet::CHAIN_BTC_SEGWIT) {
441 3
            $witnessScript = new WitnessScript($multisig);
442 3
            $redeemScript = new P2shScript($witnessScript);
443 3
            $scriptPubKey = $redeemScript->getOutputScript();
444 16
        } else if ($type === Wallet::CHAIN_BTC_DEFAULT || $type === Wallet::CHAIN_BCC_DEFAULT) {
445 15
            $witnessScript = null;
446 15
            $redeemScript = new P2shScript($multisig);
447 15
            $scriptPubKey = $redeemScript->getOutputScript();
448
        } else {
449 1
            throw new BlocktrailSDKException("Unsupported chain in path");
450
        }
451
452 15
        return new WalletScript($path, $scriptPubKey, $redeemScript, $witnessScript);
453
    }
454
455
    /**
456
     * get the path (and redeemScript) to specified address
457
     *
458
     * @param string $address
459
     * @return array
460
     */
461 1
    public function getPathForAddress($address) {
462 1
        return $this->sdk->getPathForAddress($this->identifier, $address);
463
    }
464
465
    /**
466
     * @param string|BIP32Path  $path
467
     * @return BIP32Key
468
     * @throws \Exception
469
     */
470 16
    public function getBlocktrailPublicKey($path) {
471 16
        $path = BIP32Path::path($path);
472
473 16
        $keyIndex = str_replace("'", "", $path[1]);
474
475 16
        if (!isset($this->blocktrailPublicKeys[$keyIndex])) {
476
            throw new \Exception("No blocktrail publickey for key index [{$keyIndex}]");
477
        }
478
479 16
        return $this->blocktrailPublicKeys[$keyIndex];
480
    }
481
482
    /**
483
     * generate a new derived key and return the new path and address for it
484
     *
485
     * @param int|null $chainIndex
486
     * @return string[]     [path, address]
487
     */
488 14
    public function getNewAddressPair($chainIndex = null) {
489 14
        $path = $this->getNewDerivation($chainIndex);
490 13
        $address = $this->getAddressByPath($path);
491
492 13
        return [$path, $address];
493
    }
494
495
    /**
496
     * generate a new derived private key and return the new address for it
497
     *
498
     * @param int|null $chainIndex
499
     * @return string
500
     */
501 7
    public function getNewAddress($chainIndex = null) {
502 7
        return $this->getNewAddressPair($chainIndex)[1];
503
    }
504
505
    /**
506
     * generate a new derived private key and return the new address for it
507
     *
508
     * @return string
509
     */
510 4
    public function getNewChangeAddress() {
511 4
        return $this->getNewAddressPair($this->changeIndex)[1];
512
    }
513
514
    /**
515
     * get the balance for the wallet
516
     *
517
     * @return int[]            [confirmed, unconfirmed]
518
     */
519 9
    public function getBalance() {
520 9
        $balanceInfo = $this->sdk->getWalletBalance($this->identifier);
521
522 9
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
523
    }
524
525
    /**
526
     * do wallet discovery (slow)
527
     *
528
     * @param int   $gap        the gap setting to use for discovery
529
     * @return int[]            [confirmed, unconfirmed]
530
     */
531 2
    public function doDiscovery($gap = 200) {
532 2
        $balanceInfo = $this->sdk->doWalletDiscovery($this->identifier, $gap);
533
534 2
        return [$balanceInfo['confirmed'], $balanceInfo['unconfirmed']];
535
    }
536
537
    /**
538
     * create, sign and send a transaction
539
     *
540
     * @param array    $outputs             [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send
541
     *                                      value should be INT
542
     * @param string   $changeAddress       change address to use (autogenerated if NULL)
543
     * @param bool     $allowZeroConf
544
     * @param bool     $randomizeChangeIdx  randomize the location of the change (for increased privacy / anonimity)
545
     * @param string   $feeStrategy
546
     * @param null|int $forceFee            set a fixed fee instead of automatically calculating the correct fee, not recommended!
547
     * @return string the txid / transaction hash
548
     * @throws \Exception
549
     */
550 9
    public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
551 9
        if ($this->locked) {
552 4
            throw new \Exception("Wallet needs to be unlocked to pay");
553
        }
554
555 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...
556
            throw new \InvalidArgumentException("feeStrategy should be set to force_fee to set a forced fee");
557
        }
558
559 9
        $outputs = self::normalizeOutputsStruct($outputs);
560
561 9
        $txBuilder = new TransactionBuilder();
562 9
        $txBuilder->randomizeChangeOutput($randomizeChangeIdx);
563 9
        $txBuilder->setFeeStrategy($feeStrategy);
564 9
        $txBuilder->setChangeAddress($changeAddress);
565
566 9
        foreach ($outputs as $output) {
567 9
            $txBuilder->addRecipient($output['address'], $output['value']);
568
        }
569
570 9
        $this->coinSelectionForTxBuilder($txBuilder, true, $allowZeroConf, $forceFee);
571
572 3
        $apiCheckFee = $forceFee === null;
573
574 3
        return $this->sendTx($txBuilder, $apiCheckFee);
575
    }
576
577
    /**
578
     * determine max spendable from wallet after fees
579
     *
580
     * @param bool     $allowZeroConf
581
     * @param string   $feeStrategy
582
     * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended!
583
     * @param int      $outputCnt
584
     * @return string
585
     * @throws BlocktrailSDKException
586
     */
587
    public function getMaxSpendable($allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
588
        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...
589
    }
590
591
    /**
592
     * parse outputs into normalized struct
593
     *
594
     * @param array $outputs    [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]
595
     * @return array            [['address' => address, 'value' => value], ]
596
     */
597 10
    public static function normalizeOutputsStruct(array $outputs) {
598 10
        $result = [];
599
600 10
        foreach ($outputs as $k => $v) {
601 10
            if (is_numeric($k)) {
602 1
                if (!is_array($v)) {
603
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
604
                }
605
606 1
                if (isset($v['address']) && isset($v['value'])) {
607 1
                    $address = $v['address'];
608 1
                    $value = $v['value'];
609 1
                } elseif (count($v) == 2 && isset($v[0]) && isset($v[1])) {
610 1
                    $address = $v[0];
611 1
                    $value = $v[1];
612
                } else {
613 1
                    throw new \InvalidArgumentException("outputs should be [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ]");
614
                }
615
            } else {
616 10
                $address = $k;
617 10
                $value = $v;
618
            }
619
620 10
            $result[] = ['address' => $address, 'value' => $value];
621
        }
622
623 10
        return $result;
624
    }
625
626
    /**
627
     * 'fund' the txBuilder with UTXOs (modified in place)
628
     *
629
     * @param TransactionBuilder    $txBuilder
630
     * @param bool|true             $lockUTXOs
631
     * @param bool|false            $allowZeroConf
632
     * @param null|int              $forceFee
633
     * @return TransactionBuilder
634
     */
635 11
    public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) {
636
        // get the data we should use for this transaction
637 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 635 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...
638
        
639 5
        $utxos = $coinSelection['utxos'];
640 5
        $fee = $coinSelection['fee'];
641 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...
642
643 5
        if ($forceFee !== null) {
644 1
            $txBuilder->setFee($forceFee);
645
        } else {
646 5
            $txBuilder->validateFee($fee);
647
        }
648
649 5
        foreach ($utxos as $utxo) {
650 5
            $signMode = SignInfo::MODE_SIGN;
651 5
            if (isset($utxo['sign_mode'])) {
652
                $signMode = $utxo['sign_mode'];
653
                if (!in_array($signMode, $this->allowedSignModes)) {
654
                    throw new \Exception("Sign mode disallowed by wallet");
655
                }
656
            }
657
658 5
            $txBuilder->spendOutput($utxo['hash'], $utxo['idx'], $utxo['value'], $utxo['address'], $utxo['scriptpubkey_hex'], $utxo['path'], $utxo['redeem_script'], $utxo['witness_script'], $signMode);
659
        }
660
661 5
        return $txBuilder;
662
    }
663
664
    /**
665
     * build inputs and outputs lists for TransactionBuilder
666
     *
667
     * @param TransactionBuilder $txBuilder
668
     * @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...
669
     * @throws \Exception
670
     */
671 7
    public function buildTx(TransactionBuilder $txBuilder) {
672 7
        $send = $txBuilder->getOutputs();
673 7
        $utxos = $txBuilder->getUtxos();
674 7
        $signInfo = [];
675
676 7
        $txb = new TxBuilder();
677
678 7
        foreach ($utxos as $utxo) {
679 7
            if (!$utxo->address || !$utxo->value || !$utxo->scriptPubKey) {
680 1
                $tx = $this->sdk->transaction($utxo->hash);
681
682 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...
683
                    throw new \Exception("Invalid output [{$utxo->hash}][{$utxo->index}]");
684
                }
685
686 1
                $output = $tx['outputs'][$utxo->index];
687
688 1
                if (!$utxo->address) {
689
                    $utxo->address = AddressFactory::fromString($output['address']);
690
                }
691 1
                if (!$utxo->value) {
692
                    $utxo->value = $output['value'];
693
                }
694 1
                if (!$utxo->scriptPubKey) {
695 1
                    $utxo->scriptPubKey = ScriptFactory::fromHex($output['script_hex']);
696
                }
697
            }
698
699 7
            if (SignInfo::MODE_SIGN === $utxo->signMode) {
700 7
                if (!$utxo->path) {
701
                    $utxo->path = $this->getPathForAddress($utxo->address->getAddress());
702
                }
703
704 7
                if (!$utxo->redeemScript || !$utxo->witnessScript) {
705 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...
706 6
                    $utxo->redeemScript = $redeemScript;
707 6
                    $utxo->witnessScript = $witnessScript;
708
                }
709
            }
710
711 7
            $signInfo[] = $utxo->getSignInfo();
712
        }
713
714
        $utxoSum = array_sum(array_map(function (UTXO $utxo) {
715 7
            return $utxo->value;
716 7
        }, $utxos));
717 7
        if ($utxoSum < array_sum(array_column($send, 'value'))) {
718 1
            throw new \Exception("Atempting to spend more than sum of UTXOs");
719
        }
720
721 7
        list($fee, $change) = $this->determineFeeAndChange($txBuilder, $this->getHighPriorityFeePerKB(), $this->getOptimalFeePerKB(), $this->getLowPriorityFeePerKB());
722
723 7
        if ($txBuilder->getValidateFee() !== null) {
724
            // sanity check to make sure the API isn't giving us crappy data
725 5
            if (abs($txBuilder->getValidateFee() - $fee) > (Wallet::BASE_FEE * 5)) {
726
                throw new \Exception("the fee suggested by the coin selection ({$txBuilder->getValidateFee()}) seems incorrect ({$fee})");
727
            }
728
        }
729
730 7
        if ($change > 0) {
731 5
            $send[] = [
732 5
                'address' => $txBuilder->getChangeAddress() ?: $this->getNewChangeAddress(),
733 5
                'value' => $change
734
            ];
735
        }
736
737 7
        foreach ($utxos as $utxo) {
738 7
            $txb->spendOutPoint(new OutPoint(Buffer::hex($utxo->hash), $utxo->index));
739
        }
740
741
        // outputs should be randomized to make the change harder to detect
742 7
        if ($txBuilder->shouldRandomizeChangeOuput()) {
743 7
            shuffle($send);
744
        }
745
746 7
        foreach ($send as $out) {
747 7
            assert(isset($out['value']));
748
749 7
            if (isset($out['scriptPubKey'])) {
750 2
                $txb->output($out['value'], $out['scriptPubKey']);
751 6
            } elseif (isset($out['address'])) {
752 6
                $txb->payToAddress($out['value'], AddressFactory::fromString($out['address']));
753
            } else {
754 7
                throw new \Exception();
755
            }
756
        }
757
758 7
        return [$txb->get(), $signInfo];
759
    }
760
761 7
    public function determineFeeAndChange(TransactionBuilder $txBuilder, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
762 7
        $send = $txBuilder->getOutputs();
763 7
        $utxos = $txBuilder->getUtxos();
764
765 7
        $fee = $txBuilder->getFee();
766 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...
767
768
        // if the fee is fixed we just need to calculate the change
769 7
        if ($fee !== null) {
770 2
            $change = $this->determineChange($utxos, $send, $fee);
771
772
            // if change is not dust we need to add a change output
773 2
            if ($change > Blocktrail::DUST) {
774
                $send[] = ['address' => 'change', 'value' => $change];
775
            } else {
776
                // if change is dust we add it to the fee
777 2
                $fee += $change;
778 2
                $change = 0;
779
            }
780
781 2
            return [$fee, $change];
782
        } else {
783 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
784
785 7
            $change = $this->determineChange($utxos, $send, $fee);
786
787 7
            if ($change > 0) {
788 6
                $changeIdx = count($send);
789
                // set dummy change output
790 6
                $send[$changeIdx] = ['address' => 'change', 'value' => $change];
791
792
                // recaculate fee now that we know that we have a change output
793 6
                $fee2 = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
794
795
                // unset dummy change output
796 6
                unset($send[$changeIdx]);
797
798
                // if adding the change output made the fee bump up and the change is smaller than the fee
799
                //  then we're not doing change
800 6
                if ($fee2 > $fee && $fee2 > $change) {
801 4
                    $change = 0;
802
                } else {
803 5
                    $change = $this->determineChange($utxos, $send, $fee2);
804
805
                    // if change is not dust we need to add a change output
806 5
                    if ($change > Blocktrail::DUST) {
807 5
                        $send[$changeIdx] = ['address' => 'change', 'value' => $change];
808
                    } else {
809
                        // if change is dust we do nothing (implicitly it's added to the fee)
810
                        $change = 0;
811
                    }
812
                }
813
            }
814
815
816 7
            $fee = $this->determineFee($utxos, $send, $txBuilder->getFeeStrategy(), $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB);
817
818 7
            return [$fee, $change];
819
        }
820
    }
821
822
    /**
823
     * create, sign and send transction based on TransactionBuilder
824
     *
825
     * @param TransactionBuilder $txBuilder
826
     * @param bool $apiCheckFee     let the API check if the fee is correct
827
     * @return string
828
     * @throws \Exception
829
     */
830 4
    public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) {
831 4
        list($tx, $signInfo) = $this->buildTx($txBuilder);
832
833 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...
834
    }
835
836
    /**
837
     * !! INTERNAL METHOD, public for testing purposes !!
838
     * create, sign and send transction based on inputs and outputs
839
     *
840
     * @param Transaction $tx
841
     * @param SignInfo[]  $signInfo
842
     * @param bool $apiCheckFee     let the API check if the fee is correct
843
     * @return string
844
     * @throws \Exception
845
     * @internal
846
     */
847 4
    public function _sendTx(Transaction $tx, array $signInfo, $apiCheckFee = true) {
848 4
        if ($this->locked) {
849
            throw new \Exception("Wallet needs to be unlocked to pay");
850
        }
851
852
        assert(Util::all(function ($signInfo) {
853 4
            return $signInfo instanceof SignInfo;
854 4
        }, $signInfo), '$signInfo should be SignInfo[]');
855
856
        // sign the transaction with our keys
857 4
        $signed = $this->signTransaction($tx, $signInfo);
858
859
        $txs = [
860 4
            'signed_transaction' => $signed->getHex(),
861 4
            'base_transaction' => $signed->getBaseSerialization()->getHex(),
862
        ];
863
864
        // send the transaction
865
        return $this->sendTransaction($txs, array_map(function (SignInfo $r) {
866 4
            return (string)$r->path;
867 4
        }, $signInfo), $apiCheckFee);
868
    }
869
870
    /**
871
     * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs
872
     *
873
     * @todo: mark this as deprecated, insist on the utxo's or qualified scripts.
874
     * @param int $utxoCnt      number of unspent inputs in transaction
875
     * @param int $outputCnt    number of outputs in transaction
876
     * @return float
877
     * @access public           reminder that people might use this!
878
     */
879 1
    public static function estimateFee($utxoCnt, $outputCnt) {
880 1
        $size = self::estimateSize(self::estimateSizeUTXOs($utxoCnt), self::estimateSizeOutputs($outputCnt));
881
882 1
        return self::baseFeeForSize($size);
883
    }
884
885
    /**
886
     * @param int $size     size in bytes
887
     * @return int          fee in satoshi
888
     */
889 5
    public static function baseFeeForSize($size) {
890 5
        $sizeKB = (int)ceil($size / 1000);
891
892 5
        return $sizeKB * self::BASE_FEE;
893
    }
894
895
    /**
896
     * @todo: variable varint
897
     * @todo: deprecate
898
     * @param int $txinSize
899
     * @param int $txoutSize
900
     * @return float
901
     */
902 2
    public static function estimateSize($txinSize, $txoutSize) {
903 2
        return 4 + 4 + $txinSize + 4 + $txoutSize + 4; // version + txinVarInt + txin + txoutVarInt + txout + locktime
904
    }
905
906
    /**
907
     * only supports estimating size for P2PKH/P2SH outputs
908
     *
909
     * @param int $outputCnt    number of outputs in transaction
910
     * @return float
911
     */
912 2
    public static function estimateSizeOutputs($outputCnt) {
913 2
        return ($outputCnt * 34);
914
    }
915
916
    /**
917
     * only supports estimating size for 2of3 multsig UTXOs
918
     *
919
     * @param int $utxoCnt      number of unspent inputs in transaction
920
     * @return float
921
     */
922 3
    public static function estimateSizeUTXOs($utxoCnt) {
923 3
        $txinSize = 0;
924
925 3
        for ($i=0; $i<$utxoCnt; $i++) {
926
            // @TODO: proper size calculation, we only do multisig right now so it's hardcoded and then we guess the size ...
927 3
            $multisig = "2of3";
928
929 3
            if ($multisig) {
930 3
                $sigCnt = 2;
931 3
                $msig = explode("of", $multisig);
932 3
                if (count($msig) == 2 && is_numeric($msig[0])) {
933 3
                    $sigCnt = $msig[0];
934
                }
935
936 3
                $txinSize += array_sum([
937 3
                    32, // txhash
938 3
                    4, // idx
939 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...
940 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...
941
                    (2 + 105) + // OP_PUSHDATA[>=75] + script
942
                    1, // OP_0
943 3
                    4, // sequence
944
                ]);
945
            } else {
946
                $txinSize += array_sum([
947
                    32, // txhash
948
                    4, // idx
949
                    73, // sig
950
                    34, // script
951
                    4, // sequence
952
                ]);
953
            }
954
        }
955
956 3
        return $txinSize;
957
    }
958
959
    /**
960
     * determine how much fee is required based on the inputs and outputs
961
     *  this is an estimation, not a proper 100% correct calculation
962
     *
963
     * @param UTXO[]  $utxos
964
     * @param array[] $outputs
965
     * @param         $feeStrategy
966
     * @param         $highPriorityFeePerKB
967
     * @param         $optimalFeePerKB
968
     * @param         $lowPriorityFeePerKB
969
     * @return int
970
     * @throws BlocktrailSDKException
971
     */
972 7
    protected function determineFee($utxos, $outputs, $feeStrategy, $highPriorityFeePerKB, $optimalFeePerKB, $lowPriorityFeePerKB) {
973
974 7
        $size = SizeEstimation::estimateVsize($utxos, $outputs);
975
976
        switch ($feeStrategy) {
977 7
            case self::FEE_STRATEGY_BASE_FEE:
978 4
                return self::baseFeeForSize($size);
979
980 4
            case self::FEE_STRATEGY_HIGH_PRIORITY:
981
                return (int)round(($size / 1000) * $highPriorityFeePerKB);
982
983 4
            case self::FEE_STRATEGY_OPTIMAL:
984 4
                return (int)round(($size / 1000) * $optimalFeePerKB);
985
986 1
            case self::FEE_STRATEGY_LOW_PRIORITY:
987 1
                return (int)round(($size / 1000) * $lowPriorityFeePerKB);
988
989
            case self::FEE_STRATEGY_FORCE_FEE:
990
                throw new BlocktrailSDKException("Can't determine when for force_fee");
991
992
            default:
993
                throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
994
        }
995
    }
996
997
    /**
998
     * determine how much change is left over based on the inputs and outputs and the fee
999
     *
1000
     * @param UTXO[]    $utxos
1001
     * @param array[]   $outputs
1002
     * @param int       $fee
1003
     * @return int
1004
     */
1005 7
    protected function determineChange($utxos, $outputs, $fee) {
1006
        $inputsTotal = array_sum(array_map(function (UTXO $utxo) {
1007 7
            return $utxo->value;
1008 7
        }, $utxos));
1009 7
        $outputsTotal = array_sum(array_column($outputs, 'value'));
1010
1011 7
        return $inputsTotal - $outputsTotal - $fee;
1012
    }
1013
1014
    /**
1015
     * sign a raw transaction with the private keys that we have
1016
     *
1017
     * @param Transaction $tx
1018
     * @param SignInfo[]  $signInfo
1019
     * @return TransactionInterface
1020
     * @throws \Exception
1021
     */
1022 4
    protected function signTransaction(Transaction $tx, array $signInfo) {
1023 4
        $signer = new Signer($tx, Bitcoin::getEcAdapter());
1024
1025 4
        assert(Util::all(function ($signInfo) {
1026 4
            return $signInfo instanceof SignInfo;
1027 4
        }, $signInfo), '$signInfo should be SignInfo[]');
1028
1029 4
        $sigHash = SigHash::ALL;
1030 4
        if ($this->network === "bitcoincash") {
1031
            $sigHash |= SigHash::BITCOINCASH;
1032
            $signer->redeemBitcoinCash(true);
1033
        }
1034
1035 4
        foreach ($signInfo as $idx => $info) {
1036 4
            if ($info->mode === SignInfo::MODE_SIGN) {
1037
                // required SignInfo: path, redeemScript|witnessScript, output
1038 4
                $path = BIP32Path::path($info->path)->privatePath();
1039 4
                $key = $this->primaryPrivateKey->buildKey($path)->key()->getPrivateKey();
1040 4
                $signData = new SignData();
1041 4
                if ($info->redeemScript) {
1042 4
                    $signData->p2sh($info->redeemScript);
1043
                }
1044 4
                if ($info->witnessScript) {
1045 1
                    $signData->p2wsh($info->witnessScript);
1046
                }
1047 4
                $input = $signer->input($idx, $info->output, $signData);
1048 4
                $input->sign($key, $sigHash);
1049
            }
1050
        }
1051
1052 4
        return $signer->get();
1053
    }
1054
1055
    /**
1056
     * send the transaction using the API
1057
     *
1058
     * @param string|array  $signed
1059
     * @param string[]      $paths
1060
     * @param bool          $checkFee
1061
     * @return string           the complete raw transaction
1062
     * @throws \Exception
1063
     */
1064 4
    protected function sendTransaction($signed, $paths, $checkFee = false) {
1065 4
        return $this->sdk->sendTransaction($this->identifier, $signed, $paths, $checkFee);
1066
    }
1067
1068
    /**
1069
     * @param \array[] $outputs
1070
     * @param bool $lockUTXO
1071
     * @param bool $allowZeroConf
1072
     * @param int|null|string $feeStrategy
1073
     * @param null $forceFee
1074
     * @return array
1075
     */
1076 11
    public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1077 11
        $result = $this->sdk->coinSelection($this->identifier, $outputs, $lockUTXO, $allowZeroConf, $feeStrategy, $forceFee);
1078
1079 5
        $this->highPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_HIGH_PRIORITY];
1080 5
        $this->optimalFeePerKB = $result['fees'][self::FEE_STRATEGY_OPTIMAL];
1081 5
        $this->lowPriorityFeePerKB = $result['fees'][self::FEE_STRATEGY_LOW_PRIORITY];
1082 5
        $this->feePerKBAge = time();
1083
1084 5
        return $result;
1085
    }
1086
1087 7
    public function getHighPriorityFeePerKB() {
1088 7
        if (!$this->highPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1089 4
            $this->updateFeePerKB();
1090
        }
1091
1092 7
        return $this->highPriorityFeePerKB;
1093
    }
1094
1095 7
    public function getOptimalFeePerKB() {
1096 7
        if (!$this->optimalFeePerKB || $this->feePerKBAge < time() - 60) {
1097
            $this->updateFeePerKB();
1098
        }
1099
1100 7
        return $this->optimalFeePerKB;
1101
    }
1102
1103 7
    public function getLowPriorityFeePerKB() {
1104 7
        if (!$this->lowPriorityFeePerKB || $this->feePerKBAge < time() - 60) {
1105
            $this->updateFeePerKB();
1106
        }
1107
1108 7
        return $this->lowPriorityFeePerKB;
1109
    }
1110
1111 4
    public function updateFeePerKB() {
1112 4
        $result = $this->sdk->feePerKB();
1113
1114 4
        $this->highPriorityFeePerKB = $result[self::FEE_STRATEGY_HIGH_PRIORITY];
1115 4
        $this->optimalFeePerKB = $result[self::FEE_STRATEGY_OPTIMAL];
1116 4
        $this->lowPriorityFeePerKB = $result[self::FEE_STRATEGY_LOW_PRIORITY];
1117
1118 4
        $this->feePerKBAge = time();
1119 4
    }
1120
1121
    /**
1122
     * delete the wallet
1123
     *
1124
     * @param bool $force ignore warnings (such as non-zero balance)
1125
     * @return mixed
1126
     * @throws \Exception
1127
     */
1128 10
    public function deleteWallet($force = false) {
1129 10
        if ($this->locked) {
1130
            throw new \Exception("Wallet needs to be unlocked to delete wallet");
1131
        }
1132
1133 10
        list($checksumAddress, $signature) = $this->createChecksumVerificationSignature();
1134 10
        return $this->sdk->deleteWallet($this->identifier, $checksumAddress, $signature, $force)['deleted'];
1135
    }
1136
1137
    /**
1138
     * create checksum to verify ownership of the master primary key
1139
     *
1140
     * @return string[]     [address, signature]
1141
     */
1142 10
    protected function createChecksumVerificationSignature() {
1143 10
        $privKey = $this->primaryPrivateKey->key();
1144
1145 10
        $pubKey = $this->primaryPrivateKey->publicKey();
1146 10
        $address = $pubKey->getAddress()->getAddress();
1147
1148 10
        $signer = new MessageSigner(Bitcoin::getEcAdapter());
1149 10
        $signed = $signer->sign($address, $privKey->getPrivateKey());
1150
1151 10
        return [$address, base64_encode($signed->getCompactSignature()->getBuffer()->getBinary())];
1152
    }
1153
1154
    /**
1155
     * setup a webhook for our wallet
1156
     *
1157
     * @param string    $url            URL to receive webhook events
1158
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1159
     * @return array
1160
     */
1161 1
    public function setupWebhook($url, $identifier = null) {
1162 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1163 1
        return $this->sdk->setupWalletWebhook($this->identifier, $identifier, $url);
1164
    }
1165
1166
    /**
1167
     * @param string    $identifier     identifier for the webhook, defaults to WALLET-{$this->identifier}
1168
     * @return mixed
1169
     */
1170 1
    public function deleteWebhook($identifier = null) {
1171 1
        $identifier = $identifier ?: "WALLET-{$this->identifier}";
1172 1
        return $this->sdk->deleteWalletWebhook($this->identifier, $identifier);
1173
    }
1174
1175
    /**
1176
     * lock a specific unspent output
1177
     *
1178
     * @param     $txHash
1179
     * @param     $txIdx
1180
     * @param int $ttl
1181
     * @return bool
1182
     */
1183
    public function lockUTXO($txHash, $txIdx, $ttl = 3) {
1184
        return $this->sdk->lockWalletUTXO($this->identifier, $txHash, $txIdx, $ttl);
1185
    }
1186
1187
    /**
1188
     * unlock a specific unspent output
1189
     *
1190
     * @param     $txHash
1191
     * @param     $txIdx
1192
     * @return bool
1193
     */
1194
    public function unlockUTXO($txHash, $txIdx) {
1195
        return $this->sdk->unlockWalletUTXO($this->identifier, $txHash, $txIdx);
1196
    }
1197
1198
    /**
1199
     * get all transactions for the wallet (paginated)
1200
     *
1201
     * @param  integer $page    pagination: page number
1202
     * @param  integer $limit   pagination: records per page (max 500)
1203
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1204
     * @return array            associative array containing the response
1205
     */
1206 1
    public function transactions($page = 1, $limit = 20, $sortDir = 'asc') {
1207 1
        return $this->sdk->walletTransactions($this->identifier, $page, $limit, $sortDir);
1208
    }
1209
1210
    /**
1211
     * get all addresses for the wallet (paginated)
1212
     *
1213
     * @param  integer $page    pagination: page number
1214
     * @param  integer $limit   pagination: records per page (max 500)
1215
     * @param  string  $sortDir pagination: sort direction (asc|desc)
1216
     * @return array            associative array containing the response
1217
     */
1218 1
    public function addresses($page = 1, $limit = 20, $sortDir = 'asc') {
1219 1
        return $this->sdk->walletAddresses($this->identifier, $page, $limit, $sortDir);
1220
    }
1221
1222
    /**
1223
     * get all UTXOs for the wallet (paginated)
1224
     *
1225
     * @param  integer $page        pagination: page number
1226
     * @param  integer $limit       pagination: records per page (max 500)
1227
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1228
     * @param  boolean $zeroconf    include zero confirmation transactions
1229
     * @return array                associative array containing the response
1230
     */
1231 1
    public function utxos($page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1232 1
        return $this->sdk->walletUTXOs($this->identifier, $page, $limit, $sortDir, $zeroconf);
1233
    }
1234
}
1235