Completed
Pull Request — master (#85)
by thomas
69:21
created

TransactionBuilder::getUtxos()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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