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