Passed
Pull Request — master (#129)
by thomas
26:40 queued 15:56
created

BtccomConverter::getBase58AddressHash160()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 14
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 8.8333
cc 7
nc 7
nop 1
crap 56
1
<?php
2
3
namespace Blocktrail\SDK\Backend;
4
5
use BitWasp\Bitcoin\Address\BaseAddressCreator;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Address\ScriptHashAddress;
8
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
9
use BitWasp\Bitcoin\Script\ScriptType;
10
use BitWasp\Bitcoin\Transaction\TransactionInput;
11
use Blocktrail\SDK\BlocktrailSDK;
12
use Blocktrail\SDK\Connection\Exceptions\EndpointSpecificError;
13
use Btccom\BitcoinCash\Address\CashAddress;
14 2
15 2
class BtccomConverter implements ConverterInterface {
16
17
    /**
18
     * @var BaseAddressCreator
19 2
     */
20 2
    private $addrReader;
21
22
    public function __construct(BaseAddressCreator $addrReader) {
23 2
        $this->addrReader = $addrReader;
24
    }
25
26 1
    public function paginationParams($params) {
27 1
        if (!$params) {
28
            return $params;
29
        }
30 1
31 1
        if (isset($params['limit'])) {
32
            $params['pagesize'] = $params['limit'];
33
        }
34 1
35 1
        return $params;
36
    }
37
38
    public function getUrlForBlock($blockHash) {
39
        return "block/{$blockHash}";
40
    }
41
42 1
    public function getUrlForTransaction($txId) {
43 1
        return "tx/{$txId}?verbose=3";
44
    }
45
46 1
    public function getUrlForTransactions($txIds) {
47 1
        return "tx/" . implode(",", $txIds) . "?verbose=3";
48
    }
49
50 1
    public function getUrlForBlockTransaction($blockHash) {
51 1
        return "block/{$blockHash}/tx?verbose=3";
52
    }
53
54
    public function getUrlForAddress($address) {
55
        return "address/{$address}";
56
    }
57
58 1
    public function getUrlForAddressTransactions($address) {
59 1
        return "address/{$address}/tx?verbose=3";
60
    }
61
62 3
    public function getUrlForAddressUnspent($address) {
63 3
        return "address/{$address}/unspent";
64
    }
65
66 3
    public function getUrlForBatchAddressesUnspent($addresses) {
67
        return "multi-address/" . \implode(",", $addresses) . "/unspent";
68 1
    }
69 1
70 1
    public function getUrlForAllBlocks() {
71
        return "block/list";
72 1
    }
73
74
    public function handleErros($data) {
75 1
        if (isset($data['err_no']) && $data['err_no'] > 0) {
76
            throw new EndpointSpecificError($data['err_msg'], $data['err_no']);
77 1
        }
78 1
    }
79 1
80 1
    public function convertBlock($res) {
81 1
        $data = BlocktrailSDK::jsonDecode($res, true);
82 1
        $this->handleErros($data);
83 1
84 1
        return $this->_convertBlock($data['data']);
85 1
    }
86 1
87 1
    private function _convertBlock($blockData) {
88 1
        return [
89 1
            "hash" => $blockData['hash'],
90 1
            "version" => (string)$blockData['version'],
91 1
            "height" => $blockData['height'],
92 1
            "block_time" => self::utcTimestampToISODateStr($blockData['timestamp']),
93 1
            "arrival_time" => self::utcTimestampToISODateStr($blockData['timestamp']),
94 1
            "bits" => $blockData['bits'],
95 1
            "nonce" => $blockData['nonce'],
96 1
            "merkleroot" => $blockData['mrkl_root'],
97 1
            "prev_block" => $blockData['prev_block_hash'],
98 1
            "next_block" => $blockData['next_block_hash'],
99 1
            "byte_size" => $blockData['stripped_size'],
100
            "difficulty" => (int)\floor($blockData['difficulty']),
101
            "transactions" => $blockData['tx_count'],
102
            "reward_block" => $blockData['reward_block'],
103 1
            "reward_fees" => $blockData['reward_fees'],
104 1
            "created_at" => $blockData['created_at'],
105 1
            "confirmations" => $blockData['confirmations'],
106
            "is_orphan" => $blockData['is_orphan'],
107
            "is_sw_block" => $blockData['is_sw_block'],
108 1
            "weight" => $blockData['weight'],
109 1
            "miningpool_name" => isset($blockData['miningpool_name']) ? $blockData['miningpool_name'] : null,
110 1
            "miningpool_url" => isset($blockData['miningpool_url']) ? $blockData['miningpool_url'] : null,
111 1
            "miningpool_slug" => isset($blockData['miningpool_slug']) ? $blockData['miningpool_slug'] : null
112
        ];
113
    }
114
115
    public function convertBlocks($res) {
116
        $data = BlocktrailSDK::jsonDecode($res, true);
117
        $this->handleErros($data);
118
119
        return [
120
            'data' => $data['data']['list'],
121
            'current_page' => $data['data']['page'],
122
            'per_page' => $data['data']['pagesize'],
123
            'total' => $data['data']['total_count'],
124
        ];
125
    }
126
127
    public function convertBlockTxs($res) {
128
        $data = BlocktrailSDK::jsonDecode($res, true);
129
        $this->handleErros($data);
130
131 1
        $list = array_map(function ($tx) {
132 1
            return $this->_convertTx($tx);
133 1
        }, $data['data']['list']);
134 1
135
        return [
136
            'data' => $list,
137 1
            'current_page' => $data['data']['page'],
138 1
            'per_page' => $data['data']['pagesize'],
139 1
            'total' => $data['data']['total_count'],
140
        ];
141
    }
142 1
143 1
    public function convertTx($res, $rawTx) {
144
        $data = BlocktrailSDK::jsonDecode($res, true);
145
        $this->handleErros($data);
146 1
        return $this->_convertTx($data['data']);
147 1
    }
148 1
149
    public function convertTxs($res) {
150
        $data = BlocktrailSDK::jsonDecode($res, true);
151 1
        $this->handleErros($data);
152 1
153
        return ['data' => array_map(function ($tx) {
154
            return $this->_convertTx($tx);
155 1
        }, $data['data'])];
156 1
    }
157 1
158 1
    public function convertAddressTxs($res) {
159
        $data = BlocktrailSDK::jsonDecode($res, true);
160
        $this->handleErros($data);
161
162 1
        $list = array_map(function ($tx) {
163 1
            return $this->_convertTx($tx);
164 1
        }, $data['data']['list']);
165
166
        return [
167 1
            'data' => $list,
168 1
            'current_page' => $data['data']['page'],
169 1
            'per_page' => $data['data']['pagesize'],
170 1
            'total' => $data['data']['total_count'],
171 1
        ];
172 1
    }
173 1
174 1
    public function convertAddress($res) {
175 1
        $data = BlocktrailSDK::jsonDecode($res, true);
176 1
        $this->handleErros($data);
177 1
178 1
        return [
179
            'address' => $data['data']['address'],
180
            'hash160' => self::getBase58AddressHash160($data['data']['address']),
0 ignored issues
show
Bug Best Practice introduced by
The method Blocktrail\SDK\Backend\B...tBase58AddressHash160() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

180
            'hash160' => self::/** @scrutinizer ignore-call */ getBase58AddressHash160($data['data']['address']),
Loading history...
181
            'balance' => $data['data']['balance'],
182 1
            'received' => $data['data']['received'],
183 1
            'sent' => $data['data']['sent'],
184 1
            'transactions' => $data['data']['tx_count'],
185
            'utxos' => $data['data']['unspent_tx_count'],
186 1
            'unconfirmed_received' => $data['data']['unconfirmed_received'],
187 1
            'unconfirmed_sent' => $data['data']['unconfirmed_sent'],
188 1
            'unconfirmed_transactions' => $data['data']['unconfirmed_tx_count'],
189 1
            'first_tx' => $data['data']['first_tx'],
190
            'last_tx' => $data['data']['last_tx'],
191
        ];
192 1
    }
193 1
194
    public function convertAddressUnspentOutputs($res, $address) {
195
        $data = BlocktrailSDK::jsonDecode($res, true);
196 1
        $this->handleErros($data);
197 1
198 1
        $spk = $this->addrReader->fromString($address)->getScriptPubKey();
199 1
        $type = (new OutputClassifier())->classify($spk);
200
        $scriptAsm = $spk->getScriptParser()->getHumanReadable();
201
        $scriptHex = $spk->getHex();
202
203
        $list = array_map(function ($tx) use ($address, $type, $scriptAsm, $scriptHex) {
204
            return $this->_convertUtxo($tx, $address, $type, $scriptAsm, $scriptHex);
205
        }, $data['data']['list']);
206
207
        return [
208
            'data' => $list,
209
            'current_page' => $data['data']['page'],
210
            'per_page' => $data['data']['pagesize'],
211
            'total' => $data['data']['total_count'],
212
        ];
213
    }
214
215
    public function convertBatchAddressesUnspentOutputs($res) {
216
        $data = BlocktrailSDK::jsonDecode($res, true);
217
        $this->handleErros($data);
218
219
        $list = [];
220
221
        foreach ($data['data'] as $row) {
222
            if (!$row) {
223
                continue;
224
            }
225
226
            $spk = $this->addrReader->fromString($row['address'])->getScriptPubKey();
227
            $type = (new OutputClassifier())->classify($spk);
228
            $scriptAsm = $spk->getScriptParser()->getHumanReadable();
229
            $scriptHex = $spk->getHex();
230
231
            foreach ($row['list'] as $utxo) {
232 1
                $list[] = $this->_convertUtxo($utxo, $row['address'], $type, $scriptAsm, $scriptHex);
233
            }
234 1
        }
235 1
236 1
        return [
237 1
            'data' => $list,
238 1
            'current_page' => null,
239 1
            'per_page' => null,
240 1
            'total' => count($list),
241 1
        ];
242
    }
243
244
    private function _convertUtxo($utxo, $address, $type, $scriptAsm, $scriptHex) {
245 2
        return [
246 2
            'hash' => $utxo['tx_hash'],
247 2
            'confirmations' => $utxo['confirmations'],
248 2
            'value' => $utxo['value'],
249 2
            'index' => $utxo['tx_output_n'],
250 2
            'address' => $address,
251 2
            'type' => $type,
252 2
            'script' => $scriptAsm,
253 2
            'script_hex' => $scriptHex,
254 2
        ];
255
    }
256 2
257 1
    private function _convertTx($tx) {
258
        $data = [];
259 2
        $data['size'] = $tx['vsize'];
260
        $data['hash'] = $tx['hash'];
261
        $data['block_height'] = $tx['block_height'];
262 2
        $data['block_time'] =
263 2
        $data['time'] = self::utcTimestampToISODateStr($tx['block_time']);
264 2
        $data['block_hash'] = isset($tx['block_hash']) ? $tx['block_hash'] : null;
265 2
        $data['confirmations'] = $tx['confirmations'];
266 2
        $data['is_coinbase'] = $tx['is_coinbase'];
267 2
268 2
        if ($data['is_coinbase']) {
269 2
            $totalInputValue = $tx['outputs'][0]['value'] - $tx['fee'];
270
        } else {
271 2
            $totalInputValue = $tx['inputs_value'];
272 2
        }
273
274
        $data['total_input_value'] = $totalInputValue;
275
        $data['total_output_value'] = array_reduce($tx['outputs'], function ($total, $output) {
276 2
            return $total + $output['value'];
277 2
        }, 0);
278 1
        $data['total_fee'] = $tx['fee'];
279 1
        $data['inputs'] = [];
280 1
        $data['outputs'] = [];
281 1
        $data['opt_in_rbf'] = false;
282
283 2
        foreach ($tx['inputs'] as $inputIdx => $input) {
284 2
            if ($input['sequence'] < TransactionInput::SEQUENCE_FINAL - 1) {
285 2
                $data['opt_in_rbf'] = true;
286 2
            }
287
288
            if ($data['is_coinbase'] && $input['prev_position'] === -1 &&
289 2
                $input['prev_tx_hash'] === "0000000000000000000000000000000000000000000000000000000000000000") {
290 2
                $scriptType = "coinbase";
291 2
                $inputTxid = null;
292 2
                $inputValue = $totalInputValue;
293 2
                $outpointIdx = 0;
294 2
            } else {
295 2
                $scriptType = $input['prev_type'];
296 2
                $inputValue = $input['prev_value'];
297 2
                $inputTxid = $input['prev_tx_hash'];
298
                $outpointIdx = $input['prev_position'];
299
            }
300
301
            $data['inputs'][] = [
302 2
                'index' => (int)$inputIdx,
303 2
                'output_hash' => $inputTxid,
304 2
                'output_index' => $outpointIdx,
305 2
                'value' => $inputValue,
306 2
                'sequence' => $input['sequence'],
307 2
                'address' => self::flattenAddresses($input['prev_addresses'], $scriptType),
308 2
                'type' => self::convertBtccomOutputScriptType($scriptType),
309 2
                'script_signature' => $input['script_hex'],
310 2
            ];
311 2
        }
312
313
314
        foreach ($tx['outputs'] as $outIdx => $output) {
315 2
            $data['outputs'][] = [
316 2
                'index' => (int)$outIdx,
317
                'value' => $output['value'],
318 2
                'address' => self::flattenAddresses($output['addresses'], $output['type']),
319 2
                'type' => self::convertBtccomOutputScriptType($output['type']),
320 2
                'script' => self::prettifyAsm($output['script_asm']),
321
                'script_hex' => $output['script_hex'],
322
                'spent_hash' => $output['spent_by_tx'],
323
                'spent_index' => $output['spent_by_tx_position'],
324
            ];
325
        }
326
327
        $data['size'] = $tx['size'];
328
        $data['is_double_spend'] = $tx['is_double_spend'];
329 2
330 2
        $data['lock_time_timestamp'] = null;
331 2
        $data['lock_time_block_height'] = null;
332 2
        if ($tx['lock_time']) {
333 2
            if ($tx['lock_time'] < 5000000) {
334 2
                $data['lock_time_block_height'] = $tx['lock_time'];
335
            } else {
336 2
                $data['lock_time_timestamp'] = $tx['lock_time'];
337
            }
338
        }
339 2
340 2
        // Extra fields from Btc.com
341 1
        $data['is_sw_tx'] = $tx['is_sw_tx'];
342
        $data['weight'] = $tx['weight'];
343
        $data['witness_hash'] = $tx['witness_hash'];
344 2
        $data['lock_time'] = $tx['lock_time'];
345 1
        $data['sigops'] = $tx['sigops'];
346 2
        $data['version'] = $tx['version'];
347 2
348
        return $data;
349
    }
350
351
    protected static function flattenAddresses($addresses, $type = null) {
352
        if ($type && in_array($type, ["P2WSH_V0", "P2WPKH"])) {
353 2
            return null;
354
        }
355 2
356 1
        if (!$addresses) {
357 2
            return null;
358 1
        } else if (count($addresses) === 1) {
359 2
            return $addresses[0];
360 1
        } else {
361 2
            return $addresses;
362 1
        }
363 1
    }
364
365 1
    protected static function convertBtccomOutputScriptType($scriptType) {
366
        switch ($scriptType) {
367 1
            case "P2PKH_PUBKEY":
368 1
                return "pubkey";
369
            case "P2PKH":
370
                return "pubkeyhash";
371
            case "P2SH":
372
                return "scripthash";
373
            case "P2WSH_V0":
374
                return "unknown";
375
            case "P2WPKH":
376
                return "unknown";
377
            case "NULL_DATA":
378
                return "op_return";
379
            case "coinbase":
380
                return "coinbase";
381
            default:
382
                throw new \Exception("Not implemented yet, script type: {$scriptType}");
383
        }
384
    }
385
386
    protected static function convertBitwaspScriptType($scriptType) {
387
        switch ($scriptType) {
388
            case ScriptType::P2PK:
389
                return "pubkey";
390
            case ScriptType::P2PKH:
391
                return "pubkeyhash";
392
            case ScriptType::NULLDATA:
393
                return "op_return";
394
            case ScriptType::P2SH:
395
                return "scripthash";
396
            case ScriptType::P2WSH:
397 2
                return "witnessscripthash";
398 2
            case ScriptType::P2WKH:
399
                return "witnesspubkeyhash";
400
            case ScriptType::MULTISIG:
401
            case ScriptType::WITNESS_COINBASE_COMMITMENT:
402 2
            case ScriptType::NONSTANDARD:
403
                return "unknown";
404
            default:
405 3
                throw new \Exception("Not implemented yet, script type: {$scriptType}");
406 3
        }
407
    }
408
409 1
    protected static function prettifyAsm($asm) {
410
        if (!$asm) {
411 1
            return $asm;
412
        }
413
414
        return preg_replace("/^0 /", "OP_0 ", $asm);
415
    }
416
417
    protected static function utcTimestampToISODateStr($time) {
418
        return (new \DateTime("@{$time}"))->format(\DATE_ISO8601);
419
    }
420
421
    private function getBase58AddressHash160($addr) {
422
        try {
423
            $obj = $this->addrReader->fromString($addr);
424
            if ($obj instanceof PayToPubKeyHashAddress || $obj instanceof ScriptHashAddress) {
425
                return \strtoupper($obj->getHash()->getHex());
426
            }
427
            if ($obj instanceof CashAddress && ($obj->getType() == ScriptType::P2PKH || $obj->getType() == ScriptType::P2SH)) {
428
                return \strtoupper($obj->getHash()->getHex());
429
            }
430
        } catch (\Exception $e) {
431
            // allow fallthrough, don't know this address type
432
        }
433
434
        return null;
435
    }
436
}
437