Completed
Pull Request — master (#99)
by thomas
42:14 queued 39:21
created

TransactionBuilder   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 7

Test Coverage

Coverage 64.65%

Importance

Changes 0
Metric Value
dl 0
loc 318
ccs 64
cts 99
cp 0.6465
rs 8.295
c 0
b 0
f 0
wmc 42
lcom 4
cbo 7

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getChangeAddress() 0 3 1
A getFeeStrategy() 0 3 1
A getFee() 0 3 1
A __construct() 0 4 1
B spendOutput() 0 16 8
A getUtxos() 0 3 1
A setUtxos() 0 5 1
B addRecipient() 0 22 4
A addOutput() 0 7 1
A replaceOutput() 0 5 1
A updateOutputValue() 0 18 4
A addOpReturn() 0 18 3
B getOutputs() 0 16 6
A setChangeAddress() 0 5 1
A setFeeStrategy() 0 9 2
A randomizeChangeOutput() 0 5 1
A shouldRandomizeChangeOuput() 0 3 1
A setFee() 0 10 2
A validateFee() 0 5 1
A getValidateFee() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TransactionBuilder 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 TransactionBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressInterface;
6
use BitWasp\Bitcoin\Script\ScriptFactory;
7
use BitWasp\Bitcoin\Script\ScriptInterface;
8
use BitWasp\Buffertools\Buffer;
9
use Blocktrail\SDK\Address\AddressReaderBase;
10
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
11
12
/**
13
 * Class TransactionBuilder
14
 *
15
 * still WIP so unsure if API remains the same, keep this in mind when updating the SDK!
16
 */
17
class TransactionBuilder {
18
19
    const OP_RETURN = '6a';
20
21
    /**
22
     * @var UTXO[]
23
     */
24
    private $utxos = [];
25
26
    /**
27
     * @var array[]
28
     */
29
    private $outputs = [];
30
31
    private $changeAddress = null;
32
    private $randomizeChangeOutput = true;
33
34
    private $fee = null;
35
36
    private $validateFee = null;
37
38
    private $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL;
39
40
    /**
41
     * @var AddressReaderBase
42
     */
43
    private $addressReader;
44
45
    /**
46
     * @var OutputsNormalizer
47
     */
48
    private $outputNormalizer;
49
50
    /**
51
     * TransactionBuilder constructor.
52
     * @param AddressReaderBase $addressReader
53
     */
54 13
    public function __construct(AddressReaderBase $addressReader) {
55 13
        $this->addressReader = $addressReader;
56 13
        $this->outputNormalizer = new OutputsNormalizer($this->addressReader);
57 13
    }
58
59
    /**
60
     * @param string $txId                   transactionId (hash)
61
     * @param int    $index                  index of the output being spent
62
     * @param string $value                  when NULL we'll use the data API to fetch the value
63
     * @param AddressInterface|string $address                when NULL we'll use the data API to fetch the address
64
     * @param ScriptInterface|string $scriptPubKey           as HEX, when NULL we'll use the data API to fetch the scriptpubkey
65
     * @param string $path                   when NULL we'll use the API to determine the path for the specified address
66
     * @param ScriptInterface|string $redeemScript           when NULL we'll use the path to determine the $redeemScript
67
     * @param ScriptInterface|string $witnessScript          when NULL we'll use the path to determine the $witnessScript
68
     * @return $this
69
     */
70 7
    public function spendOutput($txId, $index, $value = null, $address = null, $scriptPubKey = null, $path = null, $redeemScript = null, $witnessScript = null, $signMode = SignInfo::MODE_SIGN) {
71 7
        $address = $address instanceof AddressInterface ? $address : $this->addressReader->fromString($address);
72 7
        $scriptPubKey = ($scriptPubKey instanceof ScriptInterface)
73
            ? $scriptPubKey
74 7
            : (ctype_xdigit($scriptPubKey) ? ScriptFactory::fromHex($scriptPubKey) : null);
75 7
        $redeemScript = ($redeemScript instanceof ScriptInterface)
76
            ? $redeemScript
77 7
            : (ctype_xdigit($redeemScript) ? ScriptFactory::fromHex($redeemScript) : null);
78 7
        $witnessScript = ($witnessScript instanceof ScriptInterface)
79
            ? $witnessScript
80 7
            : (ctype_xdigit($witnessScript) ? ScriptFactory::fromHex($witnessScript) : null);
81
82 7
        $this->utxos[] = new UTXO($txId, $index, $value, $address, $scriptPubKey, $path, $redeemScript, $witnessScript, $signMode);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 70 can also be of type string; however, Blocktrail\SDK\UTXO::__construct() 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...
Bug introduced by
It seems like $path defined by parameter $path on line 70 can also be of type string; however, Blocktrail\SDK\UTXO::__construct() 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...
83
84 7
        return $this;
85
    }
86
87
    /**
88
     * @return UTXO[]
89
     */
90 7
    public function getUtxos() {
91 7
        return $this->utxos;
92
    }
93
94
    /**
95
     * replace the currently set UTXOs with a new set
96
     *
97
     * @param UTXO[] $utxos
98
     * @return $this
99
     */
100
    public function setUtxos(array $utxos) {
101
        $this->utxos = $utxos;
102
103
        return $this;
104
    }
105
106
    /**
107
     * @param string $address
108
     * @param int    $value
109
     * @return $this
110
     * @throws \Exception
111
     */
112 5
    public function addRecipient($address, $value) {
113 5
        $object = $this->addressReader->fromString($address);
114 5
        if ($object->getAddress() != $address) {
115
            throw new \Exception("Invalid address [{$address}]");
116
        }
117
118
        // using this 'dirty' way of checking for a float since there's no other reliable way in PHP
119 5
        if (!is_int($value)) {
120
            throw new \Exception("Values should be in Satoshis (int)");
121
        }
122
123 5
        if ($value <= Blocktrail::DUST) {
124
            throw new \Exception("Values should be more than dust (" . Blocktrail::DUST . ")");
125
        }
126
127 5
        $this->addOutput([
128 5
            'scriptPubKey' => $object->getScriptPubKey(),
129 5
            'value' => $value
130
        ]);
131
132 5
        return $this;
133
    }
134
135
    /**
136
     * add a 'raw' output, normally addRecipient or addOpReturn should be used
137
     *
138
     * @param array $output     [value => int, address => string]
139
     *                          or [value => int, scriptPubKey => string] (scriptPubKey should be hex)
140
     * @return $this
141
     */
142 13
    public function addOutput($output) {
143 13
        $output = $this->outputNormalizer->normalize([$output])[0];
144
145 13
        $this->outputs[] = $output;
146
147 13
        return $this;
148
    }
149
150
    /**
151
     * @param $idx
152
     * @param $output
153
     * @return $this
154
     */
155
    public function replaceOutput($idx, $output) {
156
        $this->outputs[$idx] = $output;
157
158
        return $this;
159
    }
160
161
    /**
162
     * @param $idx
163
     * @param $value
164
     * @return $this
165
     * @throws \Exception
166
     */
167
    public function updateOutputValue($idx, $value) {
168
        // using this 'dirty' way of checking for a float since there's no other reliable way in PHP
169
        if (!is_int($value)) {
170
            throw new \Exception("Values should be in Satoshis (int)");
171
        }
172
173
        if ($value <= Blocktrail::DUST) {
174
            throw new \Exception("Values should be more than dust (" . Blocktrail::DUST . ")");
175
        }
176
177
        if (!isset($this->outputs[$idx])) {
178
            throw new \Exception("No output for index [{$idx}]");
179
        }
180
181
        $this->outputs[$idx]['value'] = $value;
182
183
        return $this;
184
    }
185
186
    /**
187
     * add OP_RETURN output
188
     *
189
     * $data will be bin2hex and will be prefixed with a proper OP_PUSHDATA
190
     *
191
     * @param string $data
192
     * @param bool   $allowNonStandard  when TRUE will allow scriptPubKey > 80 bytes (so $data > 80 bytes)
193
     * @return $this
194
     * @throws BlocktrailSDKException
195
     */
196
    public function addOpReturn($data, $allowNonStandard = false) {
197
        if (!$allowNonStandard && strlen($data) / 2 > 79) {
198
            throw new BlocktrailSDKException("OP_RETURN data should be <= 79 bytes to remain standard!");
199
        }
200
201
        $script = ScriptFactory::create()
202
            ->op('OP_RETURN')
203
            ->push(new Buffer($data))
204
            ->getScript()
205
        ;
206
207
        $this->addOutput([
208
            'scriptPubKey' => $script,
209
            'value' => 0
210
        ]);
211
212
        return $this;
213
    }
214
215
    /**
216
     * @param bool $json return data for JSON return (so objects -> string)
217
     * @return array
218
     */
219
    public function getOutputs($json = false) {
220 13
        return array_map(function ($output) use ($json) {
221 13
            $result = $output;
222
223 13
            if ($json) {
224 11
                if (isset($result['scriptPubKey']) && $result['scriptPubKey'] instanceof ScriptInterface) {
225 11
                    $result['scriptPubKey'] = $result['scriptPubKey']->getHex();
226
                }
227 11
                if (isset($result['address']) && $result['address'] instanceof AddressInterface) {
228
                    $result['address'] = $result['address']->getAddress();
229
                }
230
            }
231
232 13
            return $result;
233 13
        }, $this->outputs);
234
    }
235
236
    /**
237
     * set change address
238
     *
239
     * @param string $address
240
     * @return $this
241
     */
242 10
    public function setChangeAddress($address) {
243 10
        $this->changeAddress = $address;
244
245 10
        return $this;
246
    }
247
248
    /**
249
     * @return string|null
250
     */
251 6
    public function getChangeAddress() {
252 6
        return $this->changeAddress;
253
    }
254
255
    /**
256
     * @param string $feeStrategy
257
     * @return $this
258
     * @throws BlocktrailSDKException
259
     */
260 13
    public function setFeeStrategy($feeStrategy) {
261 13
        $this->feeStrategy = $feeStrategy;
262
263 13
        if (!in_array($feeStrategy, [Wallet::FEE_STRATEGY_BASE_FEE, Wallet::FEE_STRATEGY_OPTIMAL, Wallet::FEE_STRATEGY_HIGH_PRIORITY, Wallet::FEE_STRATEGY_LOW_PRIORITY])) {
264
            throw new BlocktrailSDKException("Unknown feeStrategy [{$feeStrategy}]");
265
        }
266
267 13
        return $this;
268
    }
269
270
    /**
271
     * @return string
272
     */
273 13
    public function getFeeStrategy() {
274 13
        return $this->feeStrategy;
275
    }
276
277
    /**
278
     * @param bool $randomizeChangeOutput
279
     * @return $this
280
     */
281 10
    public function randomizeChangeOutput($randomizeChangeOutput = true) {
282 10
        $this->randomizeChangeOutput = $randomizeChangeOutput;
283
284 10
        return $this;
285
    }
286
287
    /**
288
     * @return bool
289
     */
290 7
    public function shouldRandomizeChangeOuput() {
291 7
        return $this->randomizeChangeOutput;
292
    }
293
294
    /**
295
     * set desired fee (normally automatically calculated)
296
     *
297
     * @param int $value
298
     * @return $this
299
     */
300 1
    public function setFee($value) {
301
        // using this 'dirty' way of checking for a float since there's no other reliable way in PHP
302 1
        if (!is_int($value)) {
303
            throw new \Exception("Fee should be in Satoshis (int) - can be 0");
304
        }
305
306 1
        $this->fee = $value;
307
308 1
        return $this;
309
    }
310
311
    /**
312
     * @return int|null
313
     */
314 7
    public function getFee() {
315 7
        return $this->fee;
316
    }
317
318
    /**
319
     * @param int $fee
320
     * @return $this
321
     */
322 5
    public function validateFee($fee) {
323 5
        $this->validateFee = $fee;
324
325 5
        return $this;
326
    }
327
328
    /**
329
     * @return int|null
330
     */
331 7
    public function getValidateFee() {
332 7
        return $this->validateFee;
333
    }
334
}
335