Completed
Pull Request — master (#110)
by Ruben de
75:02 queued 05:01
created

BtccomConverter::convertAddressUnspentOutputs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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