Completed
Pull Request — master (#113)
by thomas
20:21
created

Wallet   F

Complexity

Total Complexity 161

Size/Duplication

Total Lines 1255
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 37

Test Coverage

Coverage 88.5%

Importance

Changes 0
Metric Value
dl 0
loc 1255
ccs 377
cts 426
cp 0.885
rs 0.5217
c 0
b 0
f 0
wmc 161
lcom 1
cbo 37

53 Methods

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