Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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 |
||
| 16 | class Wallet implements WalletInterface { |
||
| 17 | |||
| 18 | const BASE_FEE = 10000; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * development / debug setting |
||
| 22 | * when getting a new derivation from the API, |
||
| 23 | * will verify address / redeeemScript with the values the API provides |
||
| 24 | */ |
||
| 25 | const VERIFY_NEW_DERIVATION = true; |
||
| 26 | |||
| 27 | /** |
||
| 28 | * @var BlocktrailSDKInterface |
||
| 29 | */ |
||
| 30 | protected $sdk; |
||
| 31 | |||
| 32 | /** |
||
| 33 | * @var string |
||
| 34 | */ |
||
| 35 | protected $identifier; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * BIP39 Mnemonic for the master primary private key |
||
| 39 | * |
||
| 40 | * @var string |
||
| 41 | */ |
||
| 42 | protected $primaryMnemonic; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * BIP32 master primary private key (m/) |
||
| 46 | * |
||
| 47 | * @var BIP32Key |
||
| 48 | */ |
||
| 49 | protected $primaryPrivateKey; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * @var BIP32Key[] |
||
| 53 | */ |
||
| 54 | protected $primaryPublicKeys; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * BIP32 master backup public key (M/) |
||
| 58 | |||
| 59 | * @var BIP32Key |
||
| 60 | */ |
||
| 61 | protected $backupPublicKey; |
||
| 62 | |||
| 63 | /** |
||
| 64 | * map of blocktrail BIP32 public keys |
||
| 65 | * keyed by key index |
||
| 66 | * path should be `M / key_index'` |
||
| 67 | * |
||
| 68 | * @var BIP32Key[] |
||
| 69 | */ |
||
| 70 | protected $blocktrailPublicKeys; |
||
| 71 | |||
| 72 | /** |
||
| 73 | * the 'Blocktrail Key Index' that is used for new addresses |
||
| 74 | * |
||
| 75 | * @var int |
||
| 76 | */ |
||
| 77 | protected $keyIndex; |
||
| 78 | |||
| 79 | /** |
||
| 80 | * 'bitcoin' |
||
| 81 | * |
||
| 82 | * @var string |
||
| 83 | */ |
||
| 84 | protected $network; |
||
| 85 | |||
| 86 | /** |
||
| 87 | * testnet yes / no |
||
| 88 | * |
||
| 89 | * @var bool |
||
| 90 | */ |
||
| 91 | protected $testnet; |
||
| 92 | |||
| 93 | /** |
||
| 94 | * cache of public keys, by path |
||
| 95 | * |
||
| 96 | * @var BIP32Key[] |
||
| 97 | */ |
||
| 98 | protected $pubKeys = []; |
||
| 99 | |||
| 100 | /** |
||
| 101 | * cache of address / redeemScript, by path |
||
| 102 | * |
||
| 103 | * @var string[][] [[address, redeemScript)], ] |
||
| 104 | */ |
||
| 105 | protected $derivations = []; |
||
| 106 | |||
| 107 | /** |
||
| 108 | * reverse cache of paths by address |
||
| 109 | * |
||
| 110 | * @var string[] |
||
| 111 | */ |
||
| 112 | protected $derivationsByAddress = []; |
||
| 113 | |||
| 114 | /** |
||
| 115 | * @var WalletPath |
||
| 116 | */ |
||
| 117 | protected $walletPath; |
||
| 118 | |||
| 119 | private $checksum; |
||
| 120 | |||
| 121 | private $locked = true; |
||
| 122 | |||
| 123 | protected $optimalFeePerKB; |
||
| 124 | protected $lowPriorityFeePerKB; |
||
| 125 | protected $feePerKBAge; |
||
| 126 | |||
| 127 | /** |
||
| 128 | * @param BlocktrailSDKInterface $sdk SDK instance used to do requests |
||
| 129 | * @param string $identifier identifier of the wallet |
||
| 130 | * @param string $primaryMnemonic |
||
| 131 | * @param array[string, string] $primaryPublicKeys |
||
| 132 | * @param array[string, string] $backupPublicKey should be BIP32 master public key M/ |
||
| 133 | * @param array[array[string, string]] $blocktrailPublicKeys |
||
| 134 | * @param int $keyIndex |
||
| 135 | * @param string $network |
||
| 136 | * @param bool $testnet |
||
| 137 | * @param string $checksum |
||
| 138 | */ |
||
| 139 | public function __construct(BlocktrailSDKInterface $sdk, $identifier, $primaryMnemonic, $primaryPublicKeys, $backupPublicKey, $blocktrailPublicKeys, $keyIndex, $network, $testnet, $checksum) { |
||
| 160 | |||
| 161 | /** |
||
| 162 | * return the wallet identifier |
||
| 163 | * |
||
| 164 | * @return string |
||
| 165 | */ |
||
| 166 | public function getIdentifier() { |
||
| 169 | |||
| 170 | /** |
||
| 171 | * return the wallet primary mnemonic (for backup purposes) |
||
| 172 | * |
||
| 173 | * @return string |
||
| 174 | */ |
||
| 175 | public function getPrimaryMnemonic() { |
||
| 178 | |||
| 179 | /** |
||
| 180 | * return list of Blocktrail co-sign extended public keys |
||
| 181 | * |
||
| 182 | * @return array[] [ [xpub, path] ] |
||
| 183 | */ |
||
| 184 | public function getBlocktrailPublicKeys() { |
||
| 189 | |||
| 190 | /** |
||
| 191 | * unlock wallet so it can be used for payments |
||
| 192 | * |
||
| 193 | * @param $options ['primary_private_key' => key] OR ['passphrase' => pass] |
||
| 194 | * @param callable $fn |
||
| 195 | * @return bool |
||
| 196 | * @throws \Exception |
||
| 197 | */ |
||
| 198 | public function unlock($options, callable $fn = null) { |
||
| 247 | |||
| 248 | /** |
||
| 249 | * lock the wallet (unsets primary private key) |
||
| 250 | * |
||
| 251 | * @return void |
||
| 252 | */ |
||
| 253 | public function lock() { |
||
| 257 | |||
| 258 | /** |
||
| 259 | * check if wallet is locked |
||
| 260 | * |
||
| 261 | * @return bool |
||
| 262 | */ |
||
| 263 | public function isLocked() { |
||
| 266 | |||
| 267 | /** |
||
| 268 | * upgrade wallet to different blocktrail cosign key |
||
| 269 | * |
||
| 270 | * @param $keyIndex |
||
| 271 | * @return bool |
||
| 272 | * @throws \Exception |
||
| 273 | */ |
||
| 274 | public function upgradeKeyIndex($keyIndex) { |
||
| 299 | |||
| 300 | /** |
||
| 301 | * get a new BIP32 derivation for the next (unused) address |
||
| 302 | * by requesting it from the API |
||
| 303 | * |
||
| 304 | * @return string |
||
| 305 | * @throws \Exception |
||
| 306 | */ |
||
| 307 | protected function getNewDerivation() { |
||
| 332 | |||
| 333 | /** |
||
| 334 | * @param string|BIP32Path $path |
||
| 335 | * @return BIP32Key|false |
||
| 336 | * @throws \Exception |
||
| 337 | * |
||
| 338 | * @TODO: hmm? |
||
| 339 | */ |
||
| 340 | protected function getParentPublicKey($path) { |
||
| 357 | |||
| 358 | /** |
||
| 359 | * get address for the specified path |
||
| 360 | * |
||
| 361 | * @param string|BIP32Path $path |
||
| 362 | * @return string |
||
| 363 | */ |
||
| 364 | public function getAddressByPath($path) { |
||
| 375 | |||
| 376 | /** |
||
| 377 | * get address and redeemScript for specified path |
||
| 378 | * |
||
| 379 | * @param string $path |
||
| 380 | * @return array[string, string] [address, redeemScript] |
||
| 381 | */ |
||
| 382 | public function getRedeemScriptByPath($path) { |
||
| 394 | |||
| 395 | /** |
||
| 396 | * @param BIP32Key $key |
||
| 397 | * @param string|BIP32Path $path |
||
| 398 | * @return string |
||
| 399 | */ |
||
| 400 | protected function getAddressFromKey(BIP32Key $key, $path) { |
||
| 403 | |||
| 404 | /** |
||
| 405 | * @param BIP32Key $key |
||
| 406 | * @param string|BIP32Path $path |
||
| 407 | * @return string[] [address, redeemScript] |
||
| 408 | * @throws \Exception |
||
| 409 | */ |
||
| 410 | protected function getRedeemScriptFromKey(BIP32Key $key, $path) { |
||
| 426 | |||
| 427 | /** |
||
| 428 | * get the path (and redeemScript) to specified address |
||
| 429 | * |
||
| 430 | * @param string $address |
||
| 431 | * @return array |
||
| 432 | */ |
||
| 433 | public function getPathForAddress($address) { |
||
| 436 | |||
| 437 | /** |
||
| 438 | * @param string|BIP32Path $path |
||
| 439 | * @return BIP32Key |
||
| 440 | * @throws \Exception |
||
| 441 | */ |
||
| 442 | View Code Duplication | public function getBlocktrailPublicKey($path) { |
|
| 453 | |||
| 454 | /** |
||
| 455 | * generate a new derived key and return the new path and address for it |
||
| 456 | * |
||
| 457 | * @return string[] [path, address] |
||
| 458 | */ |
||
| 459 | public function getNewAddressPair() { |
||
| 465 | |||
| 466 | /** |
||
| 467 | * generate a new derived private key and return the new address for it |
||
| 468 | * |
||
| 469 | * @return string |
||
| 470 | */ |
||
| 471 | public function getNewAddress() { |
||
| 474 | |||
| 475 | /** |
||
| 476 | * get the balance for the wallet |
||
| 477 | * |
||
| 478 | * @return int[] [confirmed, unconfirmed] |
||
| 479 | */ |
||
| 480 | public function getBalance() { |
||
| 485 | |||
| 486 | /** |
||
| 487 | * do wallet discovery (slow) |
||
| 488 | * |
||
| 489 | * @param int $gap the gap setting to use for discovery |
||
| 490 | * @return int[] [confirmed, unconfirmed] |
||
| 491 | */ |
||
| 492 | public function doDiscovery($gap = 200) { |
||
| 497 | |||
| 498 | /** |
||
| 499 | * create, sign and send a transaction |
||
| 500 | * |
||
| 501 | * @param array $outputs [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] coins to send |
||
| 502 | * value should be INT |
||
| 503 | * @param string $changeAddress change address to use (autogenerated if NULL) |
||
| 504 | * @param bool $allowZeroConf |
||
| 505 | * @param bool $randomizeChangeIdx randomize the location of the change (for increased privacy / anonimity) |
||
| 506 | * @param string $feeStrategy |
||
| 507 | * @param null|int $forceFee set a fixed fee instead of automatically calculating the correct fee, not recommended! |
||
| 508 | * @return string the txid / transaction hash |
||
| 509 | * @throws \Exception |
||
| 510 | */ |
||
| 511 | public function pay(array $outputs, $changeAddress = null, $allowZeroConf = false, $randomizeChangeIdx = true, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
||
| 532 | |||
| 533 | /** |
||
| 534 | * parse outputs into normalized struct |
||
| 535 | * |
||
| 536 | * @param array $outputs [address => value, ] or [[address, value], ] or [['address' => address, 'value' => value], ] |
||
| 537 | * @return array [['address' => address, 'value' => value], ] |
||
| 538 | */ |
||
| 539 | public static function normalizeOutputsStruct(array $outputs) { |
||
| 567 | |||
| 568 | /** |
||
| 569 | * 'fund' the txBuilder with UTXOs (modified in place) |
||
| 570 | * |
||
| 571 | * @param TransactionBuilder $txBuilder |
||
| 572 | * @param bool|true $lockUTXOs |
||
| 573 | * @param bool|false $allowZeroConf |
||
| 574 | * @param null|int $forceFee |
||
| 575 | * @return TransactionBuilder |
||
| 576 | */ |
||
| 577 | public function coinSelectionForTxBuilder(TransactionBuilder $txBuilder, $lockUTXOs = true, $allowZeroConf = false, $forceFee = null) { |
||
| 596 | |||
| 597 | /** |
||
| 598 | * build inputs and outputs lists for TransactionBuilder |
||
| 599 | * |
||
| 600 | * @param TransactionBuilder $txBuilder |
||
| 601 | * @return array |
||
| 602 | * @throws \Exception |
||
| 603 | */ |
||
| 604 | public function buildTx(TransactionBuilder $txBuilder) { |
||
| 685 | |||
| 686 | public function determineFeeAndChange(TransactionBuilder $txBuilder, $optimalFeePerKB, $lowPriorityFeePerKB) { |
||
| 742 | |||
| 743 | /** |
||
| 744 | * create, sign and send transction based on TransactionBuilder |
||
| 745 | * |
||
| 746 | * @param TransactionBuilder $txBuilder |
||
| 747 | * @param bool $apiCheckFee let the API check if the fee is correct |
||
| 748 | * @return string |
||
| 749 | * @throws \Exception |
||
| 750 | */ |
||
| 751 | public function sendTx(TransactionBuilder $txBuilder, $apiCheckFee = true) { |
||
| 756 | |||
| 757 | /** |
||
| 758 | * !! INTERNAL METHOD, public for testing purposes !! |
||
| 759 | * create, sign and send transction based on inputs and outputs |
||
| 760 | * |
||
| 761 | * @param $inputs |
||
| 762 | * @param $outputs |
||
| 763 | * @param bool $apiCheckFee let the API check if the fee is correct |
||
| 764 | * @return string |
||
| 765 | * @throws \Exception |
||
| 766 | * @internal |
||
| 767 | */ |
||
| 768 | public function _sendTx($inputs, $outputs, $apiCheckFee = true) { |
||
| 792 | |||
| 793 | /** |
||
| 794 | * only supports estimating fee for 2of3 multsig UTXOs and P2PKH/P2SH outputs |
||
| 795 | * |
||
| 796 | * @param int $utxoCnt number of unspent inputs in transaction |
||
| 797 | * @param int $outputCnt number of outputs in transaction |
||
| 798 | * @return float |
||
| 799 | * @access public reminder that people might use this! |
||
| 800 | */ |
||
| 801 | public static function estimateFee($utxoCnt, $outputCnt) { |
||
| 806 | |||
| 807 | /** |
||
| 808 | * @param int $size size in bytes |
||
| 809 | * @return int fee in satoshi |
||
| 810 | */ |
||
| 811 | public static function baseFeeForSize($size) { |
||
| 816 | |||
| 817 | /** |
||
| 818 | * @param int $txinSize |
||
| 819 | * @param int $txoutSize |
||
| 820 | * @return float |
||
| 821 | */ |
||
| 822 | public static function estimateSize($txinSize, $txoutSize) { |
||
| 825 | |||
| 826 | /** |
||
| 827 | * only supports estimating size for P2PKH/P2SH outputs |
||
| 828 | * |
||
| 829 | * @param int $outputCnt number of outputs in transaction |
||
| 830 | * @return float |
||
| 831 | */ |
||
| 832 | public static function estimateSizeOutputs($outputCnt) { |
||
| 835 | |||
| 836 | /** |
||
| 837 | * only supports estimating size for 2of3 multsig UTXOs |
||
| 838 | * |
||
| 839 | * @param int $utxoCnt number of unspent inputs in transaction |
||
| 840 | * @return float |
||
| 841 | */ |
||
| 842 | public static function estimateSizeUTXOs($utxoCnt) { |
||
| 877 | |||
| 878 | /** |
||
| 879 | * determine how much fee is required based on the inputs and outputs |
||
| 880 | * this is an estimation, not a proper 100% correct calculation |
||
| 881 | * |
||
| 882 | * @param UTXO[] $utxos |
||
| 883 | * @param array[] $outputs |
||
| 884 | * @param $feeStrategy |
||
| 885 | * @param $optimalFeePerKB |
||
| 886 | * @param $lowPriorityFeePerKB |
||
| 887 | * @return int |
||
| 888 | * @throws BlocktrailSDKException |
||
| 889 | */ |
||
| 890 | protected function determineFee($utxos, $outputs, $feeStrategy, $optimalFeePerKB, $lowPriorityFeePerKB) { |
||
| 916 | |||
| 917 | /** |
||
| 918 | * determine how much change is left over based on the inputs and outputs and the fee |
||
| 919 | * |
||
| 920 | * @param UTXO[] $utxos |
||
| 921 | * @param array[] $outputs |
||
| 922 | * @param int $fee |
||
| 923 | * @return int |
||
| 924 | */ |
||
| 925 | protected function determineChange($utxos, $outputs, $fee) { |
||
| 933 | |||
| 934 | /** |
||
| 935 | * sign a raw transaction with the private keys that we have |
||
| 936 | * |
||
| 937 | * @param string $raw_transaction |
||
| 938 | * @param array[] $inputs |
||
| 939 | * @return array response from RawTransaction::sign |
||
| 940 | * @throws \Exception |
||
| 941 | */ |
||
| 942 | protected function signTransaction($raw_transaction, array $inputs) { |
||
| 977 | |||
| 978 | /** |
||
| 979 | * send the transaction using the API |
||
| 980 | * |
||
| 981 | * @param string $signed |
||
| 982 | * @param string[] $paths |
||
| 983 | * @param bool $checkFee |
||
| 984 | * @return string the complete raw transaction |
||
| 985 | * @throws \Exception |
||
| 986 | */ |
||
| 987 | protected function sendTransaction($signed, $paths, $checkFee = false) { |
||
| 990 | |||
| 991 | /** |
||
| 992 | * use the API to get the best inputs to use based on the outputs |
||
| 993 | * |
||
| 994 | * @param array[] $outputs |
||
| 995 | * @param bool $lockUTXO |
||
| 996 | * @param bool $allowZeroConf |
||
| 997 | * @param string $feeStrategy |
||
| 998 | * @param null|int $forceFee |
||
| 999 | * @return array |
||
| 1000 | */ |
||
| 1001 | public function coinSelection($outputs, $lockUTXO = true, $allowZeroConf = false, $feeStrategy = self::FEE_STRATEGY_OPTIMAL, $forceFee = null) { |
||
| 1010 | |||
| 1011 | public function getOptimalFeePerKB() { |
||
| 1018 | |||
| 1019 | public function getLowPriorityFeePerKB() { |
||
| 1026 | |||
| 1027 | public function updateFeePerKB() { |
||
| 1035 | |||
| 1036 | /** |
||
| 1037 | * delete the wallet |
||
| 1038 | * |
||
| 1039 | * @param bool $force ignore warnings (such as non-zero balance) |
||
| 1040 | * @return mixed |
||
| 1041 | * @throws \Exception |
||
| 1042 | */ |
||
| 1043 | public function deleteWallet($force = false) { |
||
| 1051 | |||
| 1052 | /** |
||
| 1053 | * create checksum to verify ownership of the master primary key |
||
| 1054 | * |
||
| 1055 | * @return string[] [address, signature] |
||
| 1056 | */ |
||
| 1057 | protected function createChecksumVerificationSignature() { |
||
| 1065 | |||
| 1066 | /** |
||
| 1067 | * setup a webhook for our wallet |
||
| 1068 | * |
||
| 1069 | * @param string $url URL to receive webhook events |
||
| 1070 | * @param string $identifier identifier for the webhook, defaults to WALLET-{$this->identifier} |
||
| 1071 | * @return array |
||
| 1072 | */ |
||
| 1073 | public function setupWebhook($url, $identifier = null) { |
||
| 1077 | |||
| 1078 | /** |
||
| 1079 | * @param string $identifier identifier for the webhook, defaults to WALLET-{$this->identifier} |
||
| 1080 | * @return mixed |
||
| 1081 | */ |
||
| 1082 | public function deleteWebhook($identifier = null) { |
||
| 1086 | |||
| 1087 | /** |
||
| 1088 | * lock a specific unspent output |
||
| 1089 | * |
||
| 1090 | * @param $txHash |
||
| 1091 | * @param $txIdx |
||
| 1092 | * @param int $ttl |
||
| 1093 | * @return bool |
||
| 1094 | */ |
||
| 1095 | public function lockUTXO($txHash, $txIdx, $ttl = 3) { |
||
| 1098 | |||
| 1099 | /** |
||
| 1100 | * unlock a specific unspent output |
||
| 1101 | * |
||
| 1102 | * @param $txHash |
||
| 1103 | * @param $txIdx |
||
| 1104 | * @return bool |
||
| 1105 | */ |
||
| 1106 | public function unlockUTXO($txHash, $txIdx) { |
||
| 1109 | |||
| 1110 | /** |
||
| 1111 | * get all transactions for the wallet (paginated) |
||
| 1112 | * |
||
| 1113 | * @param integer $page pagination: page number |
||
| 1114 | * @param integer $limit pagination: records per page (max 500) |
||
| 1115 | * @param string $sortDir pagination: sort direction (asc|desc) |
||
| 1116 | * @return array associative array containing the response |
||
| 1117 | */ |
||
| 1118 | public function transactions($page = 1, $limit = 20, $sortDir = 'asc') { |
||
| 1121 | |||
| 1122 | /** |
||
| 1123 | * get all addresses for the wallet (paginated) |
||
| 1124 | * |
||
| 1125 | * @param integer $page pagination: page number |
||
| 1126 | * @param integer $limit pagination: records per page (max 500) |
||
| 1127 | * @param string $sortDir pagination: sort direction (asc|desc) |
||
| 1128 | * @return array associative array containing the response |
||
| 1129 | */ |
||
| 1130 | public function addresses($page = 1, $limit = 20, $sortDir = 'asc') { |
||
| 1133 | |||
| 1134 | /** |
||
| 1135 | * get all UTXOs for the wallet (paginated) |
||
| 1136 | * |
||
| 1137 | * @param integer $page pagination: page number |
||
| 1138 | * @param integer $limit pagination: records per page (max 500) |
||
| 1139 | * @param string $sortDir pagination: sort direction (asc|desc) |
||
| 1140 | * @return array associative array containing the response |
||
| 1141 | */ |
||
| 1142 | public function utxos($page = 1, $limit = 20, $sortDir = 'asc') { |
||
| 1145 | } |
||
| 1146 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.