Completed
Pull Request — master (#110)
by Ruben de
29:47
created

BtccomConverter::convertAddressUnspentOutputs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 2
dl 0
loc 20
ccs 13
cts 13
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK\Backend;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
7
use BitWasp\Bitcoin\Script\ScriptType;
8
use BitWasp\Bitcoin\Transaction\Transaction;
9
use BitWasp\Bitcoin\Transaction\TransactionInput;
10
use Blocktrail\SDK\BlocktrailSDK;
11
use Blocktrail\SDK\Connection\Exceptions\EndpointSpecificError;
12
13
class BtccomConverter implements ConverterInterface {
14 6
    public function paginationParams($params) {
15 6
        if (!$params) {
16
            return $params;
17
        }
18
19 6
        if (isset($params['limit'])) {
20 6
            $params['pagesize'] = $params['limit'];
21
        }
22
23 6
        return $params;
24
    }
25
26 1
    public function getUrlForBlock($blockHash) {
27 1
        return "block/{$blockHash}";
28
    }
29
30 5
    public function getUrlForTransaction($txId) {
31 5
        return "tx/{$txId}?verbose=3";
32
    }
33
34 1
    public function getUrlForTransactions($txIds) {
35 1
        return "tx/" . implode(",", $txIds) . "?verbose=3";
36
    }
37
38
    public function getUrlForBlockTransaction($blockHash) {
39
        return "block/{$blockHash}/tx?verbose=3";
40
    }
41
42 1
    public function getUrlForAddress($address) {
43 1
        return "address/{$address}";
44
    }
45
46 1
    public function getUrlForAddressTransactions($address) {
47 1
        return "address/{$address}/tx?verbose=3";
48
    }
49
50 1
    public function getUrlForAddressUnspent($address) {
51 1
        return "address/{$address}/unspent";
52
    }
53
54
    public function getUrlForBatchAddressesUnspent($addresses) {
55
        return "multi-address/" . \implode(",", $addresses) . "/unspent";
56
    }
57
58 1
    public function getUrlForAllBlocks() {
59 1
        return "block/list";
60
    }
61
62 7
    public function handleErros($data) {
63 7
        if (isset($data['err_no']) && $data['err_no'] > 0) {
64
            throw new EndpointSpecificError($data['err_msg'], $data['err_no']);
65
        }
66 7
    }
67
68 1
    public function convertBlock($res) {
69 1
        $data = BlocktrailSDK::jsonDecode($res, true);
70 1
        $this->handleErros($data);
71
72 1
        return $this->_convertBlock($data['data']);
73
    }
74
75 1
    private function _convertBlock($blockData) {
76
        return [
77 1
            "hash" => $blockData['hash'],
78 1
            "version" => (string)$blockData['version'],
79 1
            "height" => $blockData['height'],
80 1
            "block_time" => self::utcTimestampToISODateStr($blockData['timestamp']),
81 1
            "arrival_time" => self::utcTimestampToISODateStr($blockData['timestamp']),
82 1
            "bits" => $blockData['bits'],
83 1
            "nonce" => $blockData['nonce'],
84 1
            "merkleroot" => $blockData['mrkl_root'],
85 1
            "prev_block" => $blockData['prev_block_hash'],
86 1
            "next_block" => $blockData['next_block_hash'],
87 1
            "byte_size" => $blockData['stripped_size'],
88 1
            "difficulty" => (int)\floor($blockData['difficulty']),
89 1
            "transactions" => $blockData['tx_count'],
90 1
            "reward_block" => $blockData['reward_block'],
91 1
            "reward_fees" => $blockData['reward_fees'],
92 1
            "created_at" => $blockData['created_at'],
93 1
            "confirmations" => $blockData['confirmations'],
94 1
            "is_orphan" => $blockData['is_orphan'],
95 1
            "is_sw_block" => $blockData['is_sw_block'],
96 1
            "weight" => $blockData['weight'],
97 1
            "miningpool_name" => isset($blockData['miningpool_name']) ? $blockData['miningpool_name'] : null,
98 1
            "miningpool_url" => isset($blockData['miningpool_url']) ? $blockData['miningpool_url'] : null,
99 1
            "miningpool_slug" => isset($blockData['miningpool_slug']) ? $blockData['miningpool_slug'] : null
100
        ];
101
    }
102
103 1
    public function convertBlocks($res) {
104 1
        $data = BlocktrailSDK::jsonDecode($res, true);
105 1
        $this->handleErros($data);
106
107
        return [
108 1
            'data' => $data['data']['list'],
109 1
            'current_page' => $data['data']['page'],
110 1
            'per_page' => $data['data']['pagesize'],
111 1
            'total' => $data['data']['total_count'],
112
        ];
113
    }
114
115
    public function convertBlockTxs($res) {
116
        $data = BlocktrailSDK::jsonDecode($res, true);
117
        $this->handleErros($data);
118
119
        $list = array_map(function ($tx) {
120
            return $this->_convertTx($tx);
121
        }, $data['data']['list']);
122
123
        return [
124
            'data' => $list,
125
            'current_page' => $data['data']['page'],
126
            'per_page' => $data['data']['pagesize'],
127
            'total' => $data['data']['total_count'],
128
        ];
129
    }
130
131 5
    public function convertTx($res, $rawTx) {
132 5
        $data = BlocktrailSDK::jsonDecode($res, true);
133 5
        $this->handleErros($data);
134 5
        return $this->_convertTx($data['data']);
135
    }
136
137 1
    public function convertTxs($res) {
138 1
        $data = BlocktrailSDK::jsonDecode($res, true);
139 1
        $this->handleErros($data);
140
141
        return ['data' => array_map(function ($tx) {
142 1
            return $this->_convertTx($tx);
143 1
        }, $data['data'])];
144
    }
145
146 1
    public function convertAddressTxs($res) {
147 1
        $data = BlocktrailSDK::jsonDecode($res, true);
148 1
        $this->handleErros($data);
149
150
        $list = array_map(function ($tx) {
151 1
            return $this->_convertTx($tx);
152 1
        }, $data['data']['list']);
153
154
        return [
155 1
            'data' => $list,
156 1
            'current_page' => $data['data']['page'],
157 1
            'per_page' => $data['data']['pagesize'],
158 1
            'total' => $data['data']['total_count'],
159
        ];
160
    }
161
162 1
    public function convertAddress($res) {
163 1
        $data = BlocktrailSDK::jsonDecode($res, true);
164 1
        $this->handleErros($data);
165
166
        return [
167 1
            'address' => $data['data']['address'],
168 1
            'hash160' => self::getBase58AddressHash160($data['data']['address']),
169 1
            'balance' => $data['data']['balance'],
170 1
            'received' => $data['data']['received'],
171 1
            'sent' => $data['data']['sent'],
172 1
            'transactions' => $data['data']['tx_count'],
173 1
            'utxos' => $data['data']['unspent_tx_count'],
174 1
            'unconfirmed_received' => $data['data']['unconfirmed_received'],
175 1
            'unconfirmed_sent' => $data['data']['unconfirmed_sent'],
176 1
            'unconfirmed_transactions' => $data['data']['unconfirmed_tx_count'],
177 1
            'first_tx' => $data['data']['first_tx'],
178 1
            'last_tx' => $data['data']['last_tx'],
179
        ];
180
    }
181
182 1
    public function convertAddressUnspentOutputs($res, $address) {
183 1
        $data = BlocktrailSDK::jsonDecode($res, true);
184 1
        $this->handleErros($data);
185
186 1
        $spk = AddressFactory::fromString($address)->getScriptPubKey();
187 1
        $type = (new OutputClassifier())->classify($spk);
188 1
        $scriptAsm = $spk->getScriptParser()->getHumanReadable();
189 1
        $scriptHex = $spk->getHex();
190
191
        $list = array_map(function ($tx) use ($address, $type, $scriptAsm, $scriptHex) {
192 1
            return $this->_convertUtxo($tx, $address, $type, $scriptAsm, $scriptHex);
193 1
        }, $data['data']['list']);
194
195
        return [
196 1
            'data' => $list,
197 1
            'current_page' => $data['data']['page'],
198 1
            'per_page' => $data['data']['pagesize'],
199 1
            'total' => $data['data']['total_count'],
200
        ];
201
    }
202
203
    public function convertBatchAddressesUnspentOutputs($res) {
204
        $data = BlocktrailSDK::jsonDecode($res, true);
205
        $this->handleErros($data);
206
207
        $list = [];
208
209
        foreach ($data['data'] as $row) {
210
            $spk = AddressFactory::fromString($row['address'])->getScriptPubKey();
211
            $type = (new OutputClassifier())->classify($spk);
212
            $scriptAsm = $spk->getScriptParser()->getHumanReadable();
213
            $scriptHex = $spk->getHex();
214
215
            foreach ($row['list'] as $utxo) {
216
                $list[] = $this->_convertUtxo($utxo, $row['address'], $type, $scriptAsm, $scriptHex);
217
            }
218
        }
219
220
        return [
221
            'data' => $list,
222
            'current_page' => null,
223
            'per_page' => null,
224
            'total' => count($list),
225
        ];
226
    }
227
228 1
    private function _convertUtxo($utxo, $address, $type, $scriptAsm, $scriptHex) {
229
        return [
230 1
            'hash' => $utxo['tx_hash'],
231 1
            'confirmations' => $utxo['confirmations'],
232 1
            'value' => $utxo['value'],
233 1
            'index' => $utxo['tx_output_n'],
234 1
            'address' => $address,
235 1
            'type' => $type,
236 1
            'script' => $scriptAsm,
237 1
            'script_hex' => $scriptHex,
238
        ];
239
    }
240
241 6
    private function _convertTx($tx) {
242 6
        $data = [];
243 6
        $data['size'] = $tx['vsize'];
244 6
        $data['hash'] = $tx['hash'];
245 6
        $data['block_height'] = $tx['block_height'];
246 6
        $data['block_time'] =
247 6
        $data['time'] = self::utcTimestampToISODateStr($tx['block_time']);
248 6
        $data['block_hash'] = isset($tx['block_hash']) ? $tx['block_hash'] : null;
249 6
        $data['confirmations'] = $tx['confirmations'];
250 6
        $data['is_coinbase'] = $tx['is_coinbase'];
251
252 6
        if ($data['is_coinbase']) {
253 1
            $totalInputValue = $tx['outputs'][0]['value'] - $tx['fee'];
254
        } else {
255 6
            $totalInputValue = $tx['inputs_value'];
256
        }
257
258 6
        $data['total_input_value'] = $totalInputValue;
259 6
        $data['total_output_value'] = array_reduce($tx['outputs'], function ($total, $output) {
260 6
            return $total + $output['value'];
261 6
        }, 0);
262 6
        $data['total_fee'] = $tx['fee'];
263 6
        $data['inputs'] = [];
264 6
        $data['outputs'] = [];
265 6
        $data['opt_in_rbf'] = false;
266
267 6
        foreach ($tx['inputs'] as $inputIdx => $input) {
268 6
            if ($input['sequence'] < TransactionInput::SEQUENCE_FINAL - 1) {
269
                $data['opt_in_rbf'] = true;
270
            }
271
272 6
            if ($data['is_coinbase'] && $input['prev_position'] === -1 &&
273 6
                $input['prev_tx_hash'] === "0000000000000000000000000000000000000000000000000000000000000000") {
274 1
                $scriptType = "coinbase";
275 1
                $inputTxid = null;
276 1
                $inputValue = $totalInputValue;
277 1
                $outpointIdx = 0;
278
            } else {
279 6
                $scriptType = $input['prev_type'];
280 6
                $inputValue = $input['prev_value'];
281 6
                $inputTxid = $input['prev_tx_hash'];
282 6
                $outpointIdx = $input['prev_position'];
283
            }
284
285 6
            $data['inputs'][] = [
286 6
                'index' => (int)$inputIdx,
287 6
                'output_hash' => $inputTxid,
288 6
                'output_index' => $outpointIdx,
289 6
                'value' => $inputValue,
290 6
                'sequence' => $input['sequence'],
291 6
                'address' => self::flattenAddresses($input['prev_addresses'], $scriptType),
292 6
                'type' => self::convertBtccomOutputScriptType($scriptType),
293 6
                'script_signature' => $input['script_hex'],
294
            ];
295
        }
296
297
298 6
        foreach ($tx['outputs'] as $outIdx => $output) {
299 6
            $data['outputs'][] = [
300 6
                'index' => (int)$outIdx,
301 6
                'value' => $output['value'],
302 6
                'address' => self::flattenAddresses($output['addresses'], $output['type']),
303 6
                'type' => self::convertBtccomOutputScriptType($output['type']),
304 6
                'script' => self::prettifyAsm($output['script_asm']),
305 6
                'script_hex' => $output['script_hex'],
306 6
                'spent_hash' => $output['spent_by_tx'],
307 6
                'spent_index' => $output['spent_by_tx_position'],
308
            ];
309
        }
310
311 6
        $data['size'] = $tx['size'];
312 6
        $data['is_double_spend'] = $tx['is_double_spend'];
313
314 6
        $data['lock_time_timestamp'] = null;
315 6
        $data['lock_time_block_height'] = null;
316 6
        if ($tx['lock_time']) {
317
            if ($tx['lock_time'] < 5000000) {
318
                $data['lock_time_block_height'] = $tx['lock_time'];
319
            } else {
320
                $data['lock_time_timestamp'] = $tx['lock_time'];
321
            }
322
        }
323
324
        // Extra fields from Btc.com
325 6
        $data['is_sw_tx'] = $tx['is_sw_tx'];
326 6
        $data['weight'] = $tx['weight'];
327 6
        $data['witness_hash'] = $tx['witness_hash'];
328 6
        $data['lock_time'] = $tx['lock_time'];
329 6
        $data['sigops'] = $tx['sigops'];
330 6
        $data['version'] = $tx['version'];
331
332 6
        return $data;
333
    }
334
335 6
    protected static function flattenAddresses($addresses, $type = null) {
336 6
        if ($type && in_array($type, ["P2WSH_V0", "P2WPKH"])) {
337 1
            return null;
338
        }
339
340 6
        if (!$addresses) {
341 2
            return null;
342 6
        } else if (count($addresses) === 1) {
343 6
            return $addresses[0];
344
        } else {
345
            return $addresses;
346
        }
347
    }
348
349 6
    protected static function convertBtccomOutputScriptType($scriptType) {
350
        switch ($scriptType) {
351 6
            case "P2PKH_PUBKEY":
352 1
                return "pubkey";
353 6
            case "P2PKH":
354 1
                return "pubkeyhash";
355 6
            case "P2SH":
356 5
                return "scripthash";
357 3
            case "P2WSH_V0":
358 1
                return "unknown";
359 2
            case "P2WPKH":
360
                return "unknown";
361 2
            case "NULL_DATA":
362 1
                return "op_return";
363 1
            case "coinbase":
364 1
                return "coinbase";
365
            default:
366
                throw new \Exception("Not implemented yet, script type: {$scriptType}");
367
        }
368
    }
369
370
    protected static function convertBitwaspScriptType($scriptType) {
371
        switch ($scriptType) {
372
            case ScriptType::P2PK:
373
                return "pubkey";
374
            case ScriptType::P2PKH:
375
                return "pubkeyhash";
376
            case ScriptType::NULLDATA:
377
                return "op_return";
378
            case ScriptType::P2SH:
379
                return "scripthash";
380
            case ScriptType::P2WSH:
381
                return "witnessscripthash";
382
            case ScriptType::P2WKH:
383
                return "witnesspubkeyhash";
384
            case ScriptType::MULTISIG:
385
            case ScriptType::WITNESS_COINBASE_COMMITMENT:
386
            case ScriptType::NONSTANDARD:
387
                return "unknown";
388
            default:
389
                throw new \Exception("Not implemented yet, script type: {$scriptType}");
390
        }
391
    }
392
393 6
    protected static function prettifyAsm($asm) {
394 6
        if (!$asm) {
395
            return $asm;
396
        }
397
398 6
        return preg_replace("/^0 /", "OP_0 ", $asm);
399
    }
400
401 7
    protected static function utcTimestampToISODateStr($time) {
402 7
        return (new \DateTime("@{$time}"))->format(\DATE_ISO8601);
403
    }
404
405 1
    protected static function getBase58AddressHash160($addr) {
406
        try {
407 1
            return \strtoupper(AddressFactory::fromString($addr)->getHash()->getHex());
408
        } catch (\Exception $e) {
409
            return null;
410
        }
411
    }
412
}
413