client(); self::$rpcurl = config('lbry.rpc_url'); } /** * Execute the console command. */ public function handle(): void{ $function = $this->argument('function'); if($function){ $this->$function(); }else{ $this->warn('No arguments specified'); } } /** * @return void * @throws Throwable */ public function fixscripthashtx(): void{ $conn = DB::connection(); $otxs = Output::query()->select(['TransactionId'])->distinct(['TransactionId'])->where('Type','scripthash')->get(); foreach ($otxs as $otx) { $txid = $otx->TransactionId; $tx = Transaction::query()->select(['Hash'])->where('Id',$txid)->first(); $req = ['method' => 'getrawtransaction', 'params' => [$tx->Hash],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $tx_result = $json->result; $raw_tx = $tx_result; $tx_data = self::decode_tx($raw_tx); $all_tx_data = $this->txdb_data_from_decoded($tx_data); foreach ($all_tx_data['outputs'] as $out) { if ($out['Type'] != 'scripthash') { continue; } // get the old address $old_output = Output::query()->select(['Id', 'Addresses'])->where([ ['TransactionId',$txid], ['Vout',$out['Vout']], ])->first(); if ($old_output) { $old_addresses = json_decode($old_output->Addresses); $old_address = $old_addresses[0]; $new_addresses = json_decode($out['Addresses']); $new_address = $new_addresses[0]; // update the output with new addresses array $conn->beginTransaction(); $conn->statement('UPDATE Outputs SET Addresses = ? WHERE Id = ?', [$out['Addresses'], $old_output->Id]); // update the old address with the new one $conn->statement('UPDATE Addresses SET Address = ? WHERE Address = ?', [$new_address, $old_address]); $conn->commit(); echo "$old_address => $new_address\n"; } } } } public static function hex2str(string $hex): string{ $string = ''; for ($i = 0; $i < strlen($hex)-1; $i+=2){ $string .= chr(hexdec($hex[$i].$hex[$i+1])); } return $string; } public function updateclaimfees(): void{ self::lock('claimfees'); $conn = DB::connection(); try { $stmt = $conn->getPdo()->query('SELECT CS.Id, CS.Stream FROM ClaimStreams CS JOIN Claims C ON C.Id = CS.Id WHERE C.Fee = 0 AND C.Id <= 11462 ORDER BY Id ASC'); $claims = $stmt->fetchAll(PDO::FETCH_OBJ); foreach ($claims as $claim) { $stream = json_decode($claim->Stream); if (isset($stream->metadata->fee) && $stream->metadata->fee->amount > 0) { $fee = $stream->metadata->fee->amount; $currency = $stream->metadata->fee->currency; $conn->statement('UPDATE Claims SET Fee = ?, FeeCurrency = ? WHERE Id = ?', [$fee, $currency, $claim->Id]); echo "Updated fee for claim ID: $claim->Id. Fee: $fee, Currency: $currency\n"; } } } catch (Exception $e) { print_r($e); } self::unlock('claimfees'); } /** * @return void * @throws RedisException * @throws Throwable */ public function buildclaimindex(): void{ self::lock('buildindex'); // start with all txs $decoder_url = 'http://127.0.0.1:5000'; $conn = DB::connection(); $redis_key = 'claim.oid'; $last_claim_oid = self::$redis->exists($redis_key) ? self::$redis->get($redis_key) : 0; try { $stmt = $conn->getPdo()->query('SELECT COUNT(Id) AS RecordCount FROM Outputs WHERE Id > ?', [$last_claim_oid]); $count = min(500000, $stmt->fetch(PDO::FETCH_OBJ)->RecordCount); $idx = 0; $stmt = $conn->getPdo()->query('SELECT O.Id, O.TransactionId, O.Vout, O.ScriptPubKeyAsm, T.Hash, IFNULL(T.TransactionTime, T.CreatedTime) AS TxTime FROM Outputs O ' . 'JOIN Transactions T ON T.Id = O.TransactionId WHERE O.Id > ? ORDER BY O.Id ASC LIMIT 500000', [$last_claim_oid]); while ($out = $stmt->fetch(PDO::FETCH_OBJ)) { $idx++; $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT); $txid = $out->TransactionId; $vout = $out->Vout; if (strpos($out->ScriptPubKeyAsm, 'OP_CLAIM_NAME') !== false) { // check if the claim already exists in the claims table $stmt2 = $conn->getPdo()->query('SELECT Id FROM Claims WHERE TransactionHash = ? AND Vout = ?', [$out->Hash, $out->Vout]); $exist_claim = $stmt2->fetch(PDO::FETCH_OBJ); if ($exist_claim) { echo "[$idx_str/$count] claim already exists for [$out->Hash:$vout]. Skipping.\n"; continue; } $asm_parts = explode(' ', $out->ScriptPubKeyAsm, 4); $name_hex = $asm_parts[1]; $claim_name = @pack('H*', $name_hex); // decode claim $url = sprintf("%s/claim_decode/%s", $decoder_url, $claim_name); $json = null; try { $json = self::curl_json_get($url); } catch (Exception $e) { echo "[$idx_str/$count] claimdecode failed for [$out->Hash:$vout]. Skipping.\n"; continue; } $claim = json_decode($json); if ($claim) { $req = ['method' => 'getvalueforname', 'params' => [$claim_name],'id'=>rand()]; $json = null; try { $json = json_decode(self::curl_json_post(self::$rpcurl, json_encode($req))); if (!$json) { echo "[$idx_str/$count] getvalueforname failed for [$out->Hash:$vout]. Skipping.\n"; continue; } } catch (Exception $e) { echo "[$idx_str/$count] getvalueforname failed for [$out->Hash:$vout]. Skipping.\n"; continue; } echo "[$idx_str/$count] claim found for [$out->Hash:$vout]. Processing claim... \n"; $claim_data = []; $claim_id = $json->result->claimId; $tx_dt = DateTime::createFromFormat('U', $out->TxTime); $claim_stream_data = null; if ($claim->claimType === 'streamType') { // Build claim object to save $claim_data = [ 'ClaimId' => $claim_id, 'TransactionHash' => $out->Hash, 'Vout' => $out->Vout, 'Name' => $claim_name, 'Version' => $claim->version, 'ClaimType' => 2, // streamType 'ContentType' => isset($claim->stream->source->contentType) ? $claim->stream->source->contentType : null, 'Title' => isset($claim->stream->metadata->title) ? $claim->stream->metadata->title : null, 'Description' => isset($claim->stream->metadata->description) ? $claim->stream->metadata->description : null, 'Language' => isset($claim->stream->metadata->language) ? $claim->stream->metadata->language : null, 'Author' => isset($claim->stream->metadata->author) ? $claim->stream->metadata->author : null, 'ThumbnailUrl' => isset($claim->stream->metadata->thumbnail) ? $claim->stream->metadata->thumbnail : null, 'IsNSFW' => isset($claim->stream->metadata->nsfw) ? $claim->stream->metadata->nsfw : 0, 'Fee' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->amount : 0, 'FeeCurrency' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->currency : 0, 'Created' => $tx_dt->format('Y-m-d H:i:s'), 'Modified' => $tx_dt->format('Y-m-d H:i:s') ]; $claim_stream_data = [ 'Stream' => json_encode($claim->stream) ]; if (isset($claim->publisherSignature)) { $sig_claim = Claim::query()->select(['Id', 'ClaimId', 'Name'])->where('ClaimId',$claim->publisherSignature->certificateId)->first(); if ($sig_claim) { $claim_data['PublisherId'] = $sig_claim->ClaimId; $claim_data['PublisherName'] = $sig_claim->Name; } } } else { $claim_data = [ 'ClaimId' => $claim_id, 'TransactionHash' => $out->Hash, 'Vout' => $out->Vout, 'Name' => $claim_name, 'Version' => $claim->version, 'ClaimType' => 1, 'Certificate' => json_encode($claim->certificate), 'Created' => $tx_dt->format('Y-m-d H:i:s'), 'Modified' => $tx_dt->format('Y-m-d H:i:s') ]; } $conn->beginTransaction(); $data_error = false; $claim_entity = new Claim($claim_data); $res = $claim_entity->save(); if (!$res) { $data_error = true; echo "[$idx_str/$count] claim for [$out->Hash:$vout] FAILED to save.\n"; } if (!$data_error) { if ($claim_stream_data) { $claim_stream_data['Id'] = $claim_entity->Id; $claim_stream_entity = new ClaimStream($claim_stream_data); $res = $claim_stream_entity->save(); if (!$res) { $data_error = true; } } } if (!$data_error) { $conn->commit(); echo "[$idx_str/$count] claim for [$out->Hash:$vout] indexed.\n"; } else { $conn->rollback(); echo "[$idx_str/$count] claim for [$out->Hash:$vout] NOT indexed. Rolled back.\n"; } } else { echo "[$idx_str/$count] claim for [$out->Hash:$vout] could not be decoded. Skipping.\n"; } } else { echo "[$idx_str/$count] no claim found for [$out->Hash:$vout]. Skipping.\n"; } self::$redis->set($redis_key, $out->Id); } } catch (Exception $e) { // continue print_r($e); } self::unlock('buildindex'); } // TODO: Refactor for unique claim identification by claim_id instead of using the claim name. protected function _getclaimfortxout($pubkeyasm, $tx_hash, $vout, $tx_time = null): array{ $claim_data = null; $claim_stream_data = null; $asm_parts = explode(' ', $pubkeyasm, 4); $name_hex = $asm_parts[1]; $claim_name = @pack('H*', $name_hex); // decode claim $decoder_url = 'http://127.0.0.1:5000'; $url = sprintf("%s/claim_decode/%s", $decoder_url, $claim_name); $json = null; try { $json = self::curl_json_get($url); } catch (Exception $e) { echo "***claimdecode failed for [$tx_hash:$vout]. Skipping.\n"; } if ($json) { $claim = json_decode($json); if ($claim) { if (strpos($claim_name, '#') !== false) { $claim_name = substr($claim_name, 0, strpos($claim_name, '#')); } $req = ['method' => 'getvalueforname', 'params' => [$claim_name],'id'=>rand()]; $json = null; try { $json = json_decode(self::curl_json_post(self::$rpcurl, json_encode($req))); if ($json) { $claim_data = []; $claim_id = $json->result->claimId; $now = new DateTime('now', new DateTimeZone('UTC')); $tx_dt = ($tx_time != null) ? $tx_time : $now; if ($claim->claimType === 'streamType') { // Build claim object to save $claim_data = [ 'ClaimId' => $claim_id, 'TransactionHash' => $tx_hash, 'Vout' => $vout, 'Name' => $claim_name, 'Version' => $claim->version, 'ClaimType' => 2, // streamType 'ContentType' => isset($claim->stream->source->contentType) ? $claim->stream->source->contentType : null, 'Title' => isset($claim->stream->metadata->title) ? $claim->stream->metadata->title : null, 'Description' => isset($claim->stream->metadata->description) ? $claim->stream->metadata->description : null, 'Language' => isset($claim->stream->metadata->language) ? $claim->stream->metadata->language : null, 'Author' => isset($claim->stream->metadata->author) ? $claim->stream->metadata->author : null, 'ThumbnailUrl' => isset($claim->stream->metadata->thumbnail) ? $claim->stream->metadata->thumbnail : null, 'IsNSFW' => isset($claim->stream->metadata->nsfw) ? $claim->stream->metadata->nsfw : 0, 'Fee' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->amount : 0, 'FeeCurrency' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->currency : 0, 'Created' => $tx_dt->format('Y-m-d H:i:s'), 'Modified' => $tx_dt->format('Y-m-d H:i:s') ]; $claim_stream_data = [ 'Stream' => json_encode($claim->stream) ]; if (isset($claim->publisherSignature)) { $sig_claim = Claim::query()->select(['Id', 'ClaimId', 'Name'])->where('ClaimId',$claim->publisherSignature->certificateId)->first(); if ($sig_claim) { $claim_data['PublisherId'] = $sig_claim->ClaimId; $claim_data['PublisherName'] = $sig_claim->Name; } } } else { $claim_data = [ 'ClaimId' => $claim_id, 'TransactionHash' => $tx_hash, 'Vout' => $vout, 'Name' => $claim_name, 'Version' => $claim->version, 'ClaimType' => 1, 'Certificate' => json_encode($claim->certificate), 'Created' => $tx_dt->format('Y-m-d H:i:s'), 'Modified' => $tx_dt->format('Y-m-d H:i:s') ]; } } } catch (Exception $e) { echo "***getvalueforname failed for [$out->Hash:$vout]. Skipping.\n"; } } } return ['claim_data' => $claim_data, 'claim_stream_data' => $claim_stream_data]; } public function fixzerooutputs(): void{ self::lock('zerooutputs'); $conn = DB::connection(); /** 2017-06-12 21:38:07 **/ //$last_fixed_txid = self::$redis->exists('fix.txid') ? self::$redis->get('fix.txid') : 0; try { $stmt = $conn->getPdo()->query('SELECT Id FROM Transactions WHERE Created >= ? AND Created <= ? LIMIT 1000000', ['2017-06-15 20:44:50', '2017-06-16 08:02:09']); $txids = $stmt->fetchAll(PDO::FETCH_OBJ); $count = count($txids); $idx = 0; foreach ($txids as $distincttx) { $idx++; $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT); $txid = $distincttx->Id; echo "[$idx_str/$count] Processing txid: $txid... "; $total_diff = 0; // findtx $start_ms = round(microtime(true) * 1000); $tx = Transaction::query()->select(['Hash'])->where('Id',$txid)->first(); $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "findtx took {$diff_ms}ms. "; // Get the inputs and outputs // Get the raw transaction (Use getrawtx daemon instead (for speed!!!) // getraw $req = ['method' => 'getrawtransaction', 'params' => [$tx->Hash],'id'=>rand()]; $start_ms = round(microtime(true) * 1000); $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "getrawtx took {$diff_ms}ms. "; $start_ms = round(microtime(true) * 1000); $json = json_decode($response); $tx_result = $json->result; $raw_tx = $tx_result; $tx_data = self::decode_tx($raw_tx); $all_tx_data = $this->txdb_data_from_decoded($tx_data); $inputs = $all_tx_data['inputs']; $outputs = $all_tx_data['outputs']; $addr_id_map = []; $addr_id_drcr = []; // debits and credits grouped by address $total_tx_value = 0; $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "decodetx took {$diff_ms}ms. "; // Create / update addresses $addr_id_map = []; $new_addr_map = []; foreach($all_tx_data['addresses'] as $address => $addro) { $prev_addr = Address::query()->select(['Id'])->where('Address',$address)->first(); if (!$prev_addr) { $new_addr = [ 'Address' => $address, 'FirstSeen' => $block_ts->format('Y-m-d H:i:s') ]; $entity = new Address($new_addr); $res = $entity->save(); if (!$res) { $data_error = true; } else { $addr_id_map[$address] = $entity->Id; } $new_addr_map[$address] = 1; } else { $addr_id_map[$address] = $prev_addr->Id; } } $start_ms = round(microtime(true) * 1000); $num_outputs = count($outputs); foreach ($outputs as $out) { $vout = $out['Vout']; $total_tx_value = bcadd($total_tx_value, $out['Value'], 8); // check if the output exists $stmt = $conn->getPdo()->query('SELECT Id FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$txid, $vout]); $exist_output = $stmt->fetch(PDO::FETCH_OBJ); if (!$exist_output) { $out['TransactionId'] = $txid; $out_entity = new Output($out); //$stmt->execute('INSERT INTO Outputs') $conn->statement('INSERT INTO Outputs (TransactionId, Vout, Value, Type, ScriptPubKeyAsm, ScriptPubKeyHex, RequiredSignatures, Hash160, Addresses, Created, Modified) '. 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [$out['TransactionId'], $out['Vout'], $out['Value'], $out['Type'], $out['ScriptPubKeyAsm'], $out['ScriptPubKeyHex'], $out['RequiredSignatures'], $out['Hash160'], $out['Addresses'] ]); // get the last insert id $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS outputId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $out_entity->Id = $linsert->outputId; if ($out_entity->Id === 0) { $data_error = true; break; } $json_addr = json_decode($out['Addresses']); $address = $json_addr[0]; // Get the address ID $addr_id = -1; if (isset($addr_id_map[$address])) { $addr_id = $addr_id_map[$address]; } else { $src_addr = Address::query()->select(['Id'])->where('Address',$address)->first(); if ($src_addr) { $addr_id = $src_addr->Id; $addr_id_map[$address] = $addr_id; } } if ($addr_id > -1) { $conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out_entity->Id, $addr_id]); $conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$txid, $addr_id]); } if ($addr_id > -1 && isset($new_addr_map[$address])) { if (!isset($addr_id_drcr[$addr_id])) { $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0]; } $addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8); // Update the Received amount for the address based on the output $conn->statement('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $addr_id]); } } } $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "$num_outputs output(s) took {$diff_ms}ms. "; // Fix the input values $start_ms = round(microtime(true) * 1000); $num_inputs = count($inputs); foreach ($inputs as $in) { if (isset($in['PrevoutHash'])) { $prevout_hash = $in['PrevoutHash']; $in_prevout = $in['PrevoutN']; $prevout_tx_id = -1; $prevout_tx = Transaction::query()->select(['Id'])->where('Hash',$prevout_hash)->first(); if (!$prevout_tx) { continue; } $prevout_tx_id = $prevout_tx->Id; $stmt = $conn->getPdo()->query('SELECT Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$prevout_tx_id, $in_prevout]); $src_output = $stmt->fetch(PDO::FETCH_OBJ); if ($src_output) { $in['Value'] = $src_output->Value; //$conn->execute('UPDATE Inputs SET Value = ? WHERE TransactionId = ? AND PrevoutHash = ? AND PrevoutN = ?', [$in['Value'], $txid, $prevout_hash, $in_prevout]); // Check if the input exists $stmt = $conn->getPdo()->query('SELECT Id FROM Inputs WHERE TransactionId = ? AND PrevoutHash = ? AND PrevoutN = ?', [$txid, $prevout_hash, $in_prevout]); $exist_input = $stmt->fetch(PDO::FETCH_OBJ); if (!$exist_input) { $json_addr = json_decode($src_output->Addresses); $address = $json_addr[0]; // Get the address ID $addr_id = -1; if (isset($addr_id_map[$address])) { $addr_id = $addr_id_map[$address]; } else { $src_addr = Address::query()->select(['Id'])->where('Address',$address)->first(); if ($src_addr) { $addr_id = $src_addr->Id; $addr_id_map[$address] = $addr_id; } } $in_entity = new Input($in); $in['TransactionId'] = $txid; if ($addr_id > -1) { $in['AddressId'] = $addr_id; } $conn->statement('INSERT INTO Inputs (TransactionId, TransactionHash, AddressId, PrevoutHash, PrevoutN, Sequence, Value, ScriptSigAsm, ScriptSigHex, Created, Modified) ' . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [$in['TransactionId'], $in['TransactionHash'], isset($in['AddressId']) ? $in['AddressId'] : null, $in['PrevoutHash'], $in['PrevoutN'], $in['Sequence'], isset($in['Value']) ? $in['Value'] : 0, $in['ScriptSigAsm'], $in['ScriptSigHex'] ]); // get last insert id $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS inputId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $in_entity->Id = $linsert->inputId; if ($in_entity->Id === 0) { $data_error = true; break; } if ($addr_id > -1) { $conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in_entity->Id, $addr_id]); $conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$txid, $addr_id]); } if ($addr_id > -1 && isset($new_addr_map[$address])) { if (!isset($addr_id_drcr[$addr_id])) { $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0]; } $addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8); // Update total sent $conn->statement('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $addr_id]); } } } } } $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "$num_inputs input(s) took {$diff_ms}ms. "; // update tx time $start_ms = round(microtime(true) * 1000); $upd_addr_ids = []; //$conn->execute('UPDATE Transactions SET Value = ? WHERE Id = ?', [$total_tx_value, $txid]); foreach ($addr_id_drcr as $addr_id => $drcr) { try { $conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ? WHERE TransactionId = ? AND AddressId = ?', [$drcr['debit'], $drcr['credit'], $txid, $addr_id]); } catch (Exception $e) { print_r($e); $data_error = true; break; } } //self::$redis->set('fix.txid', $txid); $diff_ms = (round(microtime(true) * 1000)) - $start_ms; $total_diff += $diff_ms; echo "update took {$diff_ms}ms. Total {$total_diff} ms.\n"; } } catch (Exception $e) { print_r($e); } self::unlock('zerooutputs'); } public function addrtxamounts(): void{ set_time_limit(0); self::lock('addrtxamounts'); try { $conn = DB::connection(); $stmt = $conn->getPdo()->query('SELECT TransactionId, AddressId FROM TransactionsAddresses WHERE DebitAmount = 0 AND CreditAmount = 0 LIMIT 1000000'); $txaddresses = $stmt->fetchAll(PDO::FETCH_OBJ); $count = count($txaddresses); $idx = 0; echo "Processing $count tx address combos...\n"; foreach ($txaddresses as $txaddr) { $idx++; $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT); // Check the inputs $stmt = $conn->getPdo()->query('SELECT SUM(I.Value) AS DebitAmount FROM Inputs I JOIN InputsAddresses IA ON IA.InputId = I.Id WHERE I.TransactionId = ? AND IA.AddressId = ?', [$txaddr->TransactionId, $txaddr->AddressId]); $res = $stmt->fetch(PDO::FETCH_OBJ); $debitamount = $res->DebitAmount ? $res->DebitAmount : 0; $stmt = $conn->getPdo()->query('SELECT SUM(O.Value) AS CreditAmount FROM Outputs O JOIN OutputsAddresses OA ON OA.OutputId = O.Id WHERE O.TransactionId = ? AND OA.AddressId = ?', [$txaddr->TransactionId, $txaddr->AddressId]); $res = $stmt->fetch(PDO::FETCH_OBJ); $creditamount = $res->CreditAmount ? $res->CreditAmount : 0; echo "[$idx_str/$count] Updating tx $txaddr->TransactionId, address id $txaddr->AddressId with debit amount: $debitamount, credit amount: $creditamount... "; $conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ? WHERE TransactionId = ? AND AddressId = ?', [$debitamount, $creditamount, $txaddr->TransactionId, $txaddr->AddressId]); echo "Done.\n"; } } catch (Exception $e) { // failed print_r($e); } self::unlock('addrtxamounts'); } /** * @param $tx_hash * @param $block_ts * @param $block_data * @param $data_error * @return void * @throws Exception */ private function processtx($tx_hash, $block_ts, $block_data, &$data_error): void{ // Get the raw transaction (Use getrawtx daemon instead (for speed!!!) $req = ['method' => 'getrawtransaction', 'params' => [$tx_hash],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $tx_result = $json->result; $raw_tx = $tx_result; $tx_data = self::decode_tx($raw_tx); $all_tx_data = $this->txdb_data_from_decoded($tx_data); $conn = DB::connection(); // Create / update addresses $addr_id_map = []; foreach($all_tx_data['addresses'] as $address => $addrss) { $prev_addr = Address::query()->select(['Id'])->where('Address',$address)->first(); if (!$prev_addr) { $new_addr = [ 'Address' => $address, 'FirstSeen' => $block_ts->format('Y-m-d H:i:s'), ]; $entity = new Address($new_addr); $res = $entity->save(); if (!$res) { $data_error = true; } else { $addr_id_map[$address] = $entity->Id; } } else { $addr_id_map[$address] = $prev_addr->Id; } } $addr_id_drcr = []; // debits and credits grouped by address $numeric_tx_id = -1; if (!$data_error) { // Create transaction $new_tx = $all_tx_data['tx']; $total_tx_value = 0; foreach ($all_tx_data['outputs'] as $out) { $total_tx_value = bcadd($total_tx_value, $out['Value'], 8); } if ($block_data) { $new_tx['BlockHash'] = $block_data['hash']; $new_tx['TransactionTime'] = $block_data['time']; } $new_tx['TransactionSize'] = ((strlen($raw_tx)) / 2); $new_tx['InputCount'] = count($all_tx_data['inputs']); $new_tx['OutputCount'] = count($all_tx_data['outputs']); $new_tx['Hash'] = $tx_hash; $new_tx['Value'] = $total_tx_value; $new_tx['Raw'] = $raw_tx; $tx_entity = new Transaction($new_tx); $conn->statement('INSERT INTO Transactions (Version, LockTime, BlockHash, TransactionTime, InputCount, OutputCount, TransactionSize, Hash, Value, Raw, Created, Modified) VALUES ' . '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [ $new_tx['Version'], $new_tx['LockTime'], isset($new_tx['BlockHash']) ? $new_tx['BlockHash'] : null, isset($new_tx['TransactionTime']) ? $new_tx['TransactionTime'] : null, $new_tx['InputCount'], $new_tx['OutputCount'], $new_tx['TransactionSize'], $new_tx['Hash'], $new_tx['Value'], $new_tx['Raw'] ]); $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS txnId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $tx_entity->Id = $linsert->txnId; if ($tx_entity->Id === 0) { 3; } else { $numeric_tx_id = $tx_entity->Id; } } if (!$data_error && $numeric_tx_id > 0) { // Create the inputs $inputs = $all_tx_data['inputs']; $outputs = $all_tx_data['outputs']; foreach ($inputs as $in) { $in['TransactionId'] = $numeric_tx_id; $in['TransactionHash'] = $tx_hash; if (isset($in['IsCoinbase']) && $in['IsCoinbase'] === 1) { $in_entity = new Input($in); $res = $in_entity->save(); if (!$res) { $data_error = true; break; } } else { $in_tx_hash = $in['PrevoutHash']; $in_prevout = $in['PrevoutN']; // Find the corresponding previous output $in_tx = Transaction::query()->select(['Id'])->where('Hash',$in_tx_hash)->first(); $src_output = null; if ($in_tx) { $stmt = $conn->getPdo()->query('SELECT Id, Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$in_tx->Id, $in_prevout]); $src_output = $stmt->fetch(PDO::FETCH_OBJ); if ($src_output) { $in['Value'] = $src_output->Value; $json_addr = json_decode($src_output->Addresses); $in_addr_id = 0; if (isset($addr_id_map[$json_addr[0]])) { $in['AddressId'] = $addr_id_map[$json_addr[0]]; } else { $in_addr = Address::query()->select(['Id'])->where('Address',$json_addr[0])->first(); if ($in_addr) { $addr_id_map[$json_addr[0]] = $in_addr->Id; $in['AddressId'] = $in_addr->Id; } } } } $in_entity = new Input($in); $conn->statement('INSERT INTO Inputs (TransactionId, TransactionHash, AddressId, PrevoutHash, PrevoutN, Sequence, Value, ScriptSigAsm, ScriptSigHex, Created, Modified) ' . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [$in['TransactionId'], $in['TransactionHash'], isset($in['AddressId']) ? $in['AddressId'] : null, $in['PrevoutHash'], $in['PrevoutN'], $in['Sequence'], isset($in['Value']) ? $in['Value'] : 0, $in['ScriptSigAsm'], $in['ScriptSigHex'] ]); // get last insert id $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS inputId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $in_entity->Id = $linsert->inputId; if ($in_entity->Id === 0) { $data_error = true; break; } // Update the src_output spent if successful if ($src_output) { try { $conn->statement('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ? WHERE Id = ?', [$in_entity->Id, $src_output->Id]); $conn->statement('UPDATE Inputs SET PrevoutSpendUpdated = 1 WHERE Id = ?', [$in_entity->Id]); } catch (Exception $e) { $data_error = true; break; } } if (isset($in['AddressId']) && $in['AddressId'] > 0) { $addr_id = $in['AddressId']; if (!isset($addr_id_drcr[$addr_id])) { $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0]; } $addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8); try { $conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in_entity->Id, $in['AddressId']]); $conn->statement('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $in['AddressId']]); $conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $in['AddressId']]); } catch (Exception $e) { $data_error = true; break; } } } } foreach ($outputs as $out) { $out['TransactionId'] = $numeric_tx_id; $out_entity = new Output($out); //$stmt->execute('INSERT INTO Outputs') $conn->statement('INSERT INTO Outputs (TransactionId, Vout, Value, Type, ScriptPubKeyAsm, ScriptPubKeyHex, RequiredSignatures, Hash160, Addresses, Created, Modified) '. 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [$out['TransactionId'], $out['Vout'], $out['Value'], $out['Type'], $out['ScriptPubKeyAsm'], $out['ScriptPubKeyHex'], $out['RequiredSignatures'], $out['Hash160'], $out['Addresses'] ]); // get the last insert id $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS outputId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $out_entity->Id = $linsert->outputId; if ($out_entity->Id === 0) { $data_error = true; break; } $json_addr = json_decode($out['Addresses']); $out_addr_id = 0; if (isset($addr_id_map[$json_addr[0]])) { $out_addr_id = $addr_id_map[$json_addr[0]]; } else { $out_addr = Address::query()->select(['Id'])->where('Address',$json_addr[0])->first(); if ($out_addr) { $addr_id_map[$json_addr[0]] = $out_addr->Id; $out_addr_id = $out_addr->Id; } } if ($out_addr_id > 0) { $addr_id = $out_addr_id; if (!isset($addr_id_drcr[$addr_id])) { $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0]; } $addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8); try { $conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out_entity->Id, $out_addr_id]); $conn->statement('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $out_addr_id]); $conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $out_addr_id]); } catch (Exception $e) { print_r($e); $data_error = true; break; } } // create the claim if the asm pub key starts with OP_CLAIM_NAME if (strpos($out['ScriptPubKeyAsm'], 'OP_CLAIM_NAME') !== false) { $all_claim_data = $this->_getclaimfortxout($out['ScriptPubKeyAsm'], $tx_hash, $out['Vout'], $block_ts); $claim = $all_claim_data['claim_data']; $claim_stream_data = $all_claim_data['claim_stream_data']; if (!$claim) { continue; } if ($claim['ClaimType'] == 2 && !$claim_stream_data) { echo "***claim stream data missing for streamType claim\n"; continue; } $claim_entity = new Claim($claim); $res = $claim_entity->save(); if (!$res) { echo "***claim could not be saved.\n"; continue; } if (!$data_error && $claim_stream_data) { $claim_stream_data['Id'] = $claim_entity->Id; $claim_stream_entity = new ClaimStream($claim_stream_data); $res = $claim_stream_entity->save(); if (!$res) { echo "***claim stream could not be saved.\n"; } } } } } // update tx amounts if (!$data_error) { foreach ($addr_id_drcr as $addr_id => $drcr) { try { $conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ?, TransactionTime = UTC_TIMESTAMP() WHERE TransactionId = ? AND AddressId = ?', [$drcr['debit'], $drcr['credit'], $numeric_tx_id, $addr_id]); } catch (Exception $e) { print_r($e); $data_error = true; break; } } } } /** * @return void * @throws Throwable */ public function parsetxs(): void{ set_time_limit(0); self::lock('parsetxs'); // Get the minimum block with no processed transactions echo "Parsing transactions...\n"; $conn = DB::connection(); //$conn->execute('SET foreign_key_checks = 0'); //$conn->execute('SET unique_checks = 0'); try { $unproc_blocks = Block::query()->select(['Id', 'Height', 'Hash', 'TransactionHashes', 'BlockTime'])->where('TransactionsProcessed',0)->orderBy('Height')->get(); foreach ($unproc_blocks as $min_block) { $tx_hashes = json_decode($min_block->TransactionHashes); if ($tx_hashes && is_array($tx_hashes)) { $block_time = $min_block->BlockTime; $block_ts = DateTime::createFromFormat('U', $block_time); $count = count($tx_hashes); echo "Processing " . $count . " transaction(s) for block $min_block->Height ($min_block->Hash)...\n"; $data_error = false; $conn->beginTransaction(); $idx = 0; foreach ($tx_hashes as $tx_hash) { $idx++; $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT); echo "[$idx_str/$count] Processing tx hash: $tx_hash... "; $total_diff = 0; $start_ms = round(microtime(true) * 1000); $exist_tx = Transaction::query()->select(['Id'])->where('Hash', $tx_hash)->first(); $end_ms = round(microtime(true) * 1000); $diff_ms = $end_ms - $start_ms; $total_diff += $diff_ms; echo "findtx took {$diff_ms}ms. "; if ($exist_tx) { echo "Exists. Skipping.\n"; continue; } $start_ms = round(microtime(true) * 1000); $this->processtx($tx_hash, $block_ts, ['hash' => $min_block->Hash, 'time' => $min_block->BlockTime], $data_error); $diff_ms = round(microtime(true) * 1000) - $start_ms; $total_diff += $diff_ms; echo "tx took {$diff_ms}ms. Total {$total_diff}ms. "; if (!$data_error && self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) { self::$redis->srem(self::mempooltxkey, $tx_hash); echo "Removed $tx_hash from redis mempooltx.\n"; } echo "Done.\n"; } if (!$data_error) { $conn->statement('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$min_block->Id]); } if ($data_error) { echo "Rolling back!\n"; $conn->rollback(); throw new Exception('Data save failed!'); } else { echo "Data committed.\n"; $conn->commit(); } } } // Try to update txs with null BlockHash $mempooltx = Transaction::query()->select(['Id', 'Hash'])->where('BlockHash','IS','NULL')->orderBy('Created')->get(); $idx = 0; $count = count($mempooltx); foreach ($mempooltx as $tx) { $idx++; $tx_hash = $tx->Hash; $idx_str = ($count > 10 && $idx < 10) ? '0' . $idx : $idx; echo "[$idx_str/$count] Processing tx hash: $tx_hash... "; $stmt = $conn->getPdo()->query("SELECT Hash, BlockTime FROM Blocks WHERE TransactionHashes LIKE CONCAT('%', ?, '%') AND Height > ((SELECT MAX(Height) FROM Blocks) - 10000) ORDER BY Height ASC LIMIT 1", [$tx_hash]); $block = $stmt->fetch(PDO::FETCH_OBJ); if ($block) { $upd_tx = ['Id' => $tx->Id, 'BlockHash' => $block->Hash, 'TransactionTime' => $block->BlockTime]; $upd_entity = new Transaction($upd_tx); $upd_entity->save(); echo "Done.\n"; if (self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) { self::$redis->srem(self::mempooltxkey, $tx_hash); echo "Removed $tx_hash from redis mempooltx.\n"; } } else { echo "Block not found.\n"; } } } catch (Exception $e) { print_r($e); } //$conn->execute('SET foreign_key_checks = 1'); //$conn->execute('SET unique_checks = 1'); self::unlock('parsetxs'); } /** * @throws Throwable */ public function parsenewblocks(): void{ set_time_limit(0); self::lock('parsenewblocks'); echo "Parsing new blocks...\n"; self::$redis = Redis::connection()->client(); try { // Get the best block hash $req = ['method' => 'getbestblockhash', 'params' => [],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); print_r($response); print_r($json); $best_hash = $json->result; $req = ['method' => 'getblock', 'params' => [$best_hash],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $best_block = $json->result; $max_block = Block::query()->select(['Hash', 'Height'])->orderByDesc('Height')->first(); if (!$max_block) { self::unlock('parsenewblocks'); return; } $min_height = min($max_block->Height, $best_block->height); $max_height = max($max_block->Height, $best_block->height); $height_diff = $best_block->height - $max_block->Height; if ($height_diff <= 0) { self::unlock('parsenewblocks'); return; } $conn = DB::connection(); for ($curr_height = $min_height; $curr_height <= $max_height; $curr_height++) { // get the block hash $req = ['method' => 'getblockhash', 'params' => [$curr_height],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $curr_block_hash = $json->result; $next_block_hash = null; if ($curr_height < $max_height) { $req = ['method' => 'getblockhash', 'params' => [$curr_height + 1],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $next_block_hash = $json->result; } $req = ['method' => 'getblock', 'params' => [$curr_block_hash],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $curr_block = $json->result; if ($curr_block->confirmations < 0) { continue; } $next_block = null; if ($next_block_hash != null) { $req = ['method' => 'getblock', 'params' => [$next_block_hash],'id'=>rand()]; $response = self::curl_json_post(self::$rpcurl, json_encode($req)); $json = json_decode($response); $next_block = $json->result; } if ($curr_block != null) { $curr_block_ins = $this->blockdb_data_from_json($curr_block); if ($next_block != null && $curr_block_ins['NextBlockHash'] == null) { $curr_block_ins['NextBlockHash'] = $next_block->hash; } $block_data = $curr_block; $block_id = -1; // Make sure the block does not exist before inserting $old_block = Block::query()->select(['Id'])->where('Hash',$block_data->hash)->first(); if (!$old_block) { echo "Inserting block $block_data->height ($block_data->hash)... "; $curr_block_entity = new Block($curr_block_ins); $conn->statement('INSERT INTO Blocks (Bits, Chainwork, Confirmations, Difficulty, Hash, Height, MedianTime, MerkleRoot, NameClaimRoot, Nonce, PreviousBlockHash, NextBlockHash, BlockSize, Target, BlockTime, TransactionHashes, Version, VersionHex, Created, Modified) ' . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())', [ $curr_block_entity->Bits, $curr_block_entity->Chainwork, $curr_block_entity->Confirmations, $curr_block_ins['Difficulty'], // cakephp 3 why? $curr_block_entity->Hash, $curr_block_entity->Height, $curr_block_entity->MedianTime, $curr_block_entity->MerkleRoot, $curr_block_entity->NameClaimRoot, $curr_block_entity->Nonce, $curr_block_entity->PreviousBlockHash, $curr_block_entity->NextBlockHash, $curr_block_entity->BlockSize, $curr_block_entity->Target, $curr_block_entity->BlockTime, $curr_block_entity->TransactionHashes, $curr_block_entity->Version, $curr_block_entity->VersionHex, ]); $stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS lBlockId'); $linsert = $stmt->fetch(PDO::FETCH_OBJ); $curr_block_entity->Id = $linsert->lBlockId; $block_id = $curr_block_entity->Id; echo "Done.\n"; } else { echo "Updating block $block_data->height ($block_data->hash) with next block hash: " . $curr_block_ins['NextBlockHash'] . " and confirmations: " . $curr_block_ins['Confirmations'] . "... "; $upd_block = ['Id' => $old_block->Id, 'NextBlockHash' => $curr_block_ins['NextBlockHash'], 'Confirmations' => $curr_block_ins['Confirmations']]; $upd_entity = new Block($upd_block); $block_id = $old_block->Id; echo "Done.\n"; } $txs = $block_data->tx; $data_error = false; foreach ($txs as $tx_hash) { // Check if the transactions exist and then update the BlockHash and TxTime $tx = Transaction::query()->select(['Id'])->where('Hash',$tx_hash)->first(); if ($tx) { $upd_tx_data = [ 'Id' => $tx->Id, 'BlockHash' => $block_data->hash, 'TransactionTime' => $block_data->time ]; $upd_tx_entity = new Transaction($upd_tx_data); $upd_tx_entity->save(); echo "Updated tx $tx_hash with block hash and time $block_data->time.\n"; } else { // Doesn't exist, create a new transaction echo "Inserting tx $tx_hash for block height $block_data->height... "; $conn->beginTransaction(); $block_ts = \DateTime::createFromFormat('U', $block_data->time); $this->processtx($tx_hash, $block_ts, ['hash' => $block_data->hash, 'time' => $block_data->time], $data_error); if ($data_error) { $conn->rollback(); echo "Insert failed.\n"; } else { $conn->commit(); echo "Done.\n"; } } // Remove from redis if present if (!$data_error && self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) { self::$redis->srem(self::mempooltxkey, $tx_hash); echo "Removed $tx_hash from redis mempooltx.\n"; } } if (!$data_error && $block_id > -1) { // set TransactionsProcessed to true $conn->statement('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$block_id]); } } } } catch (Exception $e) { print_r($e); } self::unlock('parsenewblocks'); } /** * @throws Throwable */ public function forevermempool(): void{ self::lock('forevermempool'); $conn = DB::connection(); while (true) { try { $data = ['method' => 'getrawmempool', 'params' => [],'id'=>rand()]; $res = self::curl_json_post(self::$rpcurl, json_encode($data)); $json = json_decode($res); $txs = $json->result; $now = new DateTime('now', new DateTimeZone('UTC')); $data_error = false; if (count($txs) === 0) { // If no transactions found, that means there's nothing in the mempool. Clear redis if (self::$redis) { self::$redis->del(self::mempooltxkey); echo "Empty rawmempool. Cleared mempool txs from redis.\n"; } } foreach ($txs as $tx_hash) { // Check redis mempool txs if (self::$redis && self::$redis->exists(self::mempooltxkey)) { if (self::$redis->sismember(self::mempooltxkey, $tx_hash)) { echo "Found processed tx hash: $tx_hash. Skipping.\n"; continue; } } echo "Processing tx hash: $tx_hash... "; $exist_tx = Transaction::query()->select(['Id'])->where('Hash',$tx_hash)->first(); if ($exist_tx) { echo "Exists. Skipping.\n"; continue; } // Process the tx $conn->beginTransaction(); $block_ts = new DateTime('now', new DateTimeZone('UTC')); $this->processtx($tx_hash, $block_ts, null, $data_error); if ($data_error) { echo "Rolling back!\n"; $conn->rollback(); throw new Exception('Data save failed!'); } else { echo "Data committed.\n"; $conn->commit(); // Save to redis to prevent the DB from behing hit again if (self::$redis) { self::$redis->sadd(self::mempooltxkey, $tx_hash); } } } } catch (Exception $e) { echo "Mempool database error. Attempting to reconnect.\n"; // Final fix for MySQL server has gone away (hopefully) try { $conn->disconnect(); } catch (\Exception $e) { // ignore possible disconnect errors } $conn->reconnect(); } echo "*******************\n"; sleep(1); } self::unlock('forevermempool'); } private function txdb_data_from_decoded($decoded_tx): array{ $tx = [ 'Version' => $decoded_tx['version'], 'LockTime' => $decoded_tx['locktime'] ]; $addresses = []; $inputs = []; $outputs = []; $vin = $decoded_tx['vin']; $vout = $decoded_tx['vout']; if (is_array($vin)) { foreach ($vin as $in) { if (isset($in['coinbase'])) { $inputs[] = [ 'IsCoinbase' => 1, 'Coinbase' => $in['coinbase'] ]; } else { $inputs[] = [ 'PrevoutHash' => $in['txid'], 'PrevoutN' => $in['vout'], 'ScriptSigAsm' => $in['scriptSig']['asm'], 'ScriptSigHex' => $in['scriptSig']['hex'], 'Sequence' => $in['sequence'] ]; } } foreach ($vout as $out) { $outputs[] = [ 'Vout' => $out['vout'], 'Value' => bcdiv($out['value'], 100000000, 8), 'Type' => isset($out['scriptPubKey']['type']) ? $out['scriptPubKey']['type'] : '', 'ScriptPubKeyAsm' => isset($out['scriptPubKey']['asm']) ? $out['scriptPubKey']['asm'] : '', 'ScriptPubKeyHex' => isset($out['scriptPubKey']['hex']) ? $out['scriptPubKey']['hex'] : '', 'RequiredSignatures' => isset($out['scriptPubKey']['reqSigs']) ? $out['scriptPubKey']['reqSigs'] : '', 'Hash160' => isset($out['scriptPubKey']['hash160']) ? $out['scriptPubKey']['hash160'] : '', 'Addresses' => isset($out['scriptPubKey']['addresses']) ? json_encode($out['scriptPubKey']['addresses']) : null ]; if (isset($out['scriptPubKey']['addresses'])) { foreach ($out['scriptPubKey']['addresses'] as $address) { $addresses[$address] = $address; } } } } return ['tx' => $tx, 'addresses' => $addresses, 'inputs' => $inputs, 'outputs' => $outputs]; } private function blockdb_data_from_json($json_block): array{ return [ 'Bits' => $json_block->bits, 'Chainwork' => $json_block->chainwork, 'Confirmations' => $json_block->confirmations, 'Difficulty' => $json_block->difficulty, 'Hash' => $json_block->hash, 'Height' => $json_block->height, 'MedianTime' => $json_block->mediantime, 'MerkleRoot' => $json_block->merkleroot, 'NameClaimRoot' => $json_block->nameclaimroot, 'Nonce' => $json_block->nonce, 'PreviousBlockHash' => isset($json_block->previousblockhash) ? $json_block->previousblockhash : null, 'NextBlockHash' => isset($json_block->nextblockhash) ? $json_block->nextblockhash : null, 'BlockSize' => $json_block->size, 'Target' => $json_block->target ?? null,//TODO: just $json_block->target 'BlockTime' => $json_block->time, 'TransactionHashes' => json_encode($json_block->tx), 'Version' => $json_block->version, 'VersionHex' => $json_block->versionHex ]; } /** * @param $url * @param $data * @param $headers * @return bool|string * @throws Exception */ private static function curl_json_post($url, $data, $headers = []): string{ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); //Log::debug('Request execution completed.'); if ($response === false) { $error = curl_error($ch); $errno = curl_errno($ch); curl_close($ch); throw new Exception(sprintf('The request failed: %s', $error), $errno); } else { curl_close($ch); } // Close any open file handle return $response; } private static function curl_json_get($url, $headers = []): string{ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); //Log::debug('Request execution completed.'); if ($response === false) { $error = curl_error($ch); $errno = curl_errno($ch); curl_close($ch); throw new \Exception(sprintf('The request failed: %s', $error), $errno); } else { curl_close($ch); } // Close any open file handle return $response; } private static $base58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; public static $op_codes = [ ['OP_0', 0], ['OP_PUSHDATA', 76], 'OP_PUSHDATA2', 'OP_PUSHDATA4', 'OP_1NEGATE', 'OP_RESERVED', 'OP_1', 'OP_2', 'OP_3', 'OP_4', 'OP_5', 'OP_6', 'OP_7', 'OP_8', 'OP_9', 'OP_10', 'OP_11', 'OP_12', 'OP_13', 'OP_14', 'OP_15', 'OP_16', 'OP_NOP', 'OP_VER', 'OP_IF', 'OP_NOTIF', 'OP_VERIF', 'OP_VERNOTIF', 'OP_ELSE', 'OP_ENDIF', 'OP_VERIFY', 'OP_RETURN', 'OP_TOALTSTACK', 'OP_FROMALTSTACK', 'OP_2DROP', 'OP_2DUP', 'OP_3DUP', 'OP_2OVER', 'OP_2ROT', 'OP_2SWAP', 'OP_IFDUP', 'OP_DEPTH', 'OP_DROP', 'OP_DUP', 'OP_NIP', 'OP_OVER', 'OP_PICK', 'OP_ROLL', 'OP_ROT', 'OP_SWAP', 'OP_TUCK', 'OP_CAT', 'OP_SUBSTR', 'OP_LEFT', 'OP_RIGHT', 'OP_SIZE', 'OP_INVERT', 'OP_AND', 'OP_OR', 'OP_XOR', 'OP_EQUAL', 'OP_EQUALVERIFY', 'OP_RESERVED1', 'OP_RESERVED2', 'OP_1ADD', 'OP_1SUB', 'OP_2MUL', 'OP_2DIV', 'OP_NEGATE', 'OP_ABS', 'OP_NOT', 'OP_0NOTEQUAL', 'OP_ADD', 'OP_SUB', 'OP_MUL', 'OP_DIV', 'OP_MOD', 'OP_LSHIFT', 'OP_RSHIFT', 'OP_BOOLAND', 'OP_BOOLOR', 'OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_NUMNOTEQUAL', 'OP_LESSTHAN', 'OP_GREATERTHAN', 'OP_LESSTHANOREQUAL', 'OP_GREATERTHANOREQUAL', 'OP_MIN', 'OP_MAX', 'OP_WITHIN', 'OP_RIPEMD160', 'OP_SHA1', 'OP_SHA256', 'OP_HASH160', 'OP_HASH256', 'OP_CODESEPARATOR', 'OP_CHECKSIG', 'OP_CHECKSIGVERIFY', 'OP_CHECKMULTISIG', 'OP_CHECKMULTISIGVERIFY', 'OP_NOP1', 'OP_NOP2', 'OP_NOP3', 'OP_NOP4', 'OP_NOP5', 'OP_CLAIM_NAME', 'OP_SUPPORT_CLAIM', 'OP_UPDATE_CLAIM', ['OP_SINGLEBYTE_END', 0xF0], ['OP_DOUBLEBYTE_BEGIN', 0xF000], 'OP_PUBKEY', 'OP_PUBKEYHASH', ['OP_INVALIDOPCODE', 0xFFFF] ]; public static $op_code = [ '00' => 'OP_0', // or OP_FALSE '51' => 'OP_1', // or OP_TRUE '61' => 'OP_NOP', '6a' => 'OP_RETURN', '6d' => 'OP_2DROP', '75' => 'OP_DROP', '76' => 'OP_DUP', '87' => 'OP_EQUAL', '88' => 'OP_EQUALVERIFY', 'a6' => 'OP_RIPEMD160', 'a7' => 'OP_SHA1', 'a8' => 'OP_SHA256', 'a9' => 'OP_HASH160', 'aa' => 'OP_HASH256', 'ac' => 'OP_CHECKSIG', 'ae' => 'OP_CHECKMULTISIG', 'b5' => 'OP_CLAIM_NAME', 'b6' => 'OP_SUPPORT_CLAIM', 'b7' => 'OP_UPDATE_CLAIM' ]; /*protected static function hash160_to_bc_address($h160, $addrType = 0) { $vh160 = $c . $h160; $h = self::_dhash($vh160); $addr = $vh160 . substr($h, 0, 4); return $addr; //return self::base58_encode($addr); }*/ protected static function _dhash($str, $raw = false): string{ return hash('sha256', hash('sha256', $str, true), $raw); } public static function _get_vint(&$string): int{ // Load the next byte, convert to decimal. $decimal = hexdec(self::_return_bytes($string, 1)); // Less than 253: Not encoding extra bytes. // More than 253, work out the $number of bytes using the 2^(offset) $num_bytes = ($decimal < 253) ? 0 : 2 ^ ($decimal - 253); // Num_bytes is 0: Just return the decimal // Otherwise, return $num_bytes bytes (order flipped) and converted to decimal return ($num_bytes == 0) ? $decimal : hexdec(self::_return_bytes($string, $num_bytes, true)); } public static function hash160($string): string{ $bs = @pack("H*", $string); return hash("ripemd160", hash("sha256", $bs, true)); } public static function hash256($string): string{ $bs = @pack("H*", $string); return hash("sha256", hash("sha256", $bs, true)); } public static function hash160_to_address($hash160, $address_version = null): string{ $c = ''; if ($address_version == self::pubKeyAddress[0]) { $c = dechex(self::pubKeyAddress[1]); } else if ($address_version == self::scriptAddress[0]) { $c = dechex(self::scriptAddress[1]); } $hash160 = $c . $hash160; $addr = $hash160; return self::base58_encode_checksum($addr); } public static function base58_encode_checksum($hex): string{ $checksum = self::hash256($hex); $checksum = substr($checksum, 0, 8); $hash = $hex . $checksum; return self::base58_encode($hash); } public static function _decode_script($script): string{ $pos = 0; $data = array(); while ($pos < strlen($script)) { $code = hexdec(substr($script, $pos, 2)); // hex opcode. $pos += 2; if ($code < 1) { // OP_FALSE $push = '0'; } elseif ($code <= 75) { // $code bytes will be pushed to the stack. $push = substr($script, $pos, ($code * 2)); $pos += $code * 2; } elseif ($code <= 78) { // In this range, 2^($code-76) is the number of bytes to take for the *next* number onto the stack. $szsz = pow(2, $code - 75); // decimal number of bytes. $sz = hexdec(substr($script, $pos, ($szsz * 2))); // decimal number of bytes to load and push. $pos += $szsz; $push = substr($script, $pos, ($pos + $sz * 2)); // Load the data starting from the new position. $pos += $sz * 2; } elseif ($code <= 108/*96*/) { // OP_x, where x = $code-80 $push = ($code - 80); } else { $push = $code; } $data[] = $push; } return implode(" ", $data); } /*public static function script_getopname($bytes_hex) { $index = hexdec($bytes_hex) - 75; $op = self::$op_codes[$index]; if (is_array($op)) { if ($bytes_hex == dechex($op[1])) { return $op[0]; } } if () return str_replace('OP_', '', $op); }*/ public static function get_opcode($opname): ?int{ $len = count(self::$op_codes); for ($i = 0; $i < $len; $i++) { $op = self::$op_codes[$i]; $op = is_array($op) ? $op[0] : $op; if ($op === $opname) { return $i; } } return null; } public static function match_decoded($decoded, $to_match): bool{ if (strlen($decoded) != strlen($to_match)) { return false; } for ($i = 0; $i < count($decoded); $i++) { $pushdata4 = self::get_opcode('OP_PUSHDATA4'); if ($to_match[$i] == $pushdata4 && ($pushdata4 >= $decoded[$i][0]) > 0) { continue; } if ($to_match[$i] != $decoded[$i][0]) { return false; } } return true; } public static function base58_encode($hex): string{ if (strlen($hex) == 0) { return ''; } // Convert the hex string to a base10 integer $num = gmp_strval(gmp_init($hex, 16), 58); // Check that number isn't just 0 - which would be all padding. if ($num != '0') { $num = strtr($num, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv', self::$base58chars); } else { $num = ''; } // Pad the leading 1's $pad = ''; $n = 0; while (substr($hex, $n, 2) == '00') { $pad .= '1'; $n += 2; } return $pad . $num; } public static function decode_tx($raw_transaction): array{ $math = EccFactory::getAdapter(); /*$magic_byte = BitcoinLib::magicByte($magic_byte); $magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/ $raw_transaction = trim($raw_transaction); if (((bool)preg_match('/^[0-9a-fA-F]{2,}$/i', $raw_transaction) !== true) || (strlen($raw_transaction)) % 2 !== 0 ) { throw new \InvalidArgumentException("Raw transaction is invalid hex"); } $txHash = hash('sha256', hash('sha256', pack("H*", trim($raw_transaction)), true)); $txid = self::_flip_byte_order($txHash); $info = array(); $info['txid'] = $txid; $info['version'] = $math->hexDec(self::_return_bytes($raw_transaction, 4, true)); /*if (!in_array($info['version'], array('0', '1'))) { throw new \InvalidArgumentException("Invalid transaction version"); }*/ $input_count = self::_get_vint($raw_transaction); if (!($input_count >= 0 && $input_count <= 4294967296)) { throw new \InvalidArgumentException("Invalid input count"); } $info['vin'] = self::_decode_inputs($raw_transaction, $input_count); if ($info['vin'] == false) { throw new \InvalidArgumentException("No inputs in transaction"); } $output_count = self::_get_vint($raw_transaction); if (!($output_count >= 0 && $output_count <= 4294967296)) { throw new \InvalidArgumentException("Invalid output count"); } $info['vout'] = self::_decode_outputs($raw_transaction, $output_count); $info['locktime'] = $math->hexDec(self::_return_bytes($raw_transaction, 4)); return $info; } public static function _decode_inputs(&$raw_transaction, $input_count): array{ $inputs = array(); // Loop until $input count is reached, sequentially removing the // leading data from $raw_transaction reference. for ($i = 0; $i < $input_count; $i++) { // Load the TxID (32bytes) and vout (4bytes) $txid = self::_return_bytes($raw_transaction, 32, true); $vout = self::_return_bytes($raw_transaction, 4, true); // Script is prefixed with a varint that must be decoded. $script_length = self::_get_vint($raw_transaction); // decimal number of bytes. $script = self::_return_bytes($raw_transaction, $script_length); // Build input body depending on whether the TxIn is coinbase. if ($txid == '0000000000000000000000000000000000000000000000000000000000000000') { $input_body = array('coinbase' => $script); } else { $input_body = array('txid' => $txid, 'vout' => hexdec($vout), 'scriptSig' => array('asm' => self::_decode_script($script), 'hex' => $script)); } // Append a sequence number, and finally add the input to the array. $input_body['sequence'] = hexdec(self::_return_bytes($raw_transaction, 4)); $inputs[$i] = $input_body; } return $inputs; } public static function _decode_outputs(&$tx, $output_count): array{ $math = EccFactory::getAdapter(); /*$magic_byte = BitcoinLib::magicByte($magic_byte); $magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/ $outputs = array(); for ($i = 0; $i < $output_count; $i++) { // Pop 8 bytes (flipped) from the $tx string, convert to decimal, // and then convert to Satoshis. $satoshis = $math->hexDec(self::_return_bytes($tx, 8, true)); // Decode the varint for the length of the scriptPubKey $script_length = self::_get_vint($tx); // decimal number of bytes $script = self::_return_bytes($tx, $script_length); try { $asm = self::_decode_scriptPubKey($script); } catch (\Exception $e) { $asm = null; } // Begin building scriptPubKey $scriptPubKey = array( 'asm' => $asm, 'hex' => $script ); // Try to decode the scriptPubKey['asm'] to learn the transaction type. $txn_info = self::_get_transaction_type($scriptPubKey['asm']); if ($txn_info !== false) { $scriptPubKey = array_merge($scriptPubKey, $txn_info); } $outputs[$i] = array( 'value' => $satoshis, 'vout' => $i, 'scriptPubKey' => $scriptPubKey); } return $outputs; } /** * @throws Exception */ public function fixoutputs(): void{ $sql = 'SELECT * FROM Outputs WHERE Id NOT IN (SELECT OutputId FROM OutputsAddresses)'; $conn = DB::connection(); $stmt = $conn->getPdo()->query($sql); $outs = $stmt->fetchAll(PDO::FETCH_OBJ); foreach ($outs as $out) { $txn_info = self::_get_transaction_type($out->ScriptPubKeyAsm); $out_data = [ 'Id' => $out->Id, 'Type' => $txn_info['type'], 'RequiredSignatures' => $txn_info['reqSigs'], 'Hash160' => $txn_info['hash160'], 'Addresses' => json_encode($txn_info['addresses']) ]; $out_entity = new Output($out_data); $out_entity->save(); // Fix the addresses foreach ($txn_info['addresses'] as $address) { $prev_addr = Address::query()->where('Address',$address)->first(); $addr_id = -1; if ($prev_addr) { $addr_id = $prev_addr->Id; } else { $dt = new DateTime($out->Created, new DateTimeZone('UTC')); $new_addr = [ 'Address' => $address, 'FirstSeen' => $dt->format('Y-m-d H:i:s') ]; $new_addr_entity = new Address($new_addr); if ($new_addr_entity->save()) { $addr_id = $new_addr_entity->Id; } } if ($addr_id > -1) { $conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out->Id, $addr_id]); } } echo "Fixed output $out->Id with new data: " . print_r($out_data, true); } } public function fixinputs(): void{ $sql = 'SELECT * FROM Inputs WHERE IsCoinbase <> 1 AND Id NOT IN (SELECT InputId FROM InputsAddresses)'; $conn = DB::connection(); $stmt = $conn->getPdo()->query($sql); $ins = $stmt->fetchAll(PDO::FETCH_OBJ); foreach ($ins as $in) { $prev_tx_hash = $in->PrevoutHash; $prev_n = $in->PrevoutN; // Get the previous transaction $prev_tx = Transaction::query()->select(['Id'])->where('Hash',$prev_tx_hash)->first(); if (!$prev_tx) { echo "Previous tx for hash $prev_tx_hash not found.\n"; continue; } $prev_tx_id = $prev_tx->Id; $src_output = Output::query()->where('TransactionId',$prev_tx_id)->where('Vout',$prev_n)->first(); //TODO: ->contain(['OutputAddresses']) $in_data = ['Id' => $in->Id]; if ($src_output) { $in_data['Value'] = $src_output->Value; $in_data['AddressId'] = $src_output->OutputAddresses[0]->Id; $in_entity = new Input($in_data); if ($in_entity->save()) { $conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in->Id, $in_data['AddressId']]); } } echo "Fixed input $in->Id with new data: " . print_r($in_data, true); } } public static function _get_transaction_type($data): mixed{ //$magic_byte = BitcoinLib::magicByte($magic_byte); //$magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte); $has_claim = (strpos($data, 'CLAIM') !== false); $has_update_claim = (strpos($data, 'UPDATE_CLAIM') !== false); $has_op_0 = (strpos($data, 'OP_0') !== false); $data = explode(" ", trim($data)); // Define information about eventual transactions cases, and // the position of the hash160 address in the stack. $define = array(); $rule = array(); // Other standard: pay to pubkey hash $define['p2pk'] = array('type' => 'pubkeyhash', 'reqSigs' => 1, 'data_index_for_hash' => 0); $rule['p2pk'] = [ '0' => '/^[0-9a-f]+$/i', '1' => '/^OP_CHECKSIG/' ]; // Pay to script hash $define['p2sh'] = array('type' => 'scripthash', 'reqSigs' => 1, 'data_index_for_hash' => 1); $rule['p2sh'] = array( '0' => '/^OP_HASH160/', '1' => '/^[0-9a-f]{40}$/i', // pos 1 '2' => '/^OP_EQUAL/'); // Non-standard (claim_name and support_claim) $define['p2c'] = array('type' => 'nonstandard', 'reqSigs' => 1, 'data_index_for_hash' => 7); $rule['p2c'] = [ '0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/', '1' => '/^[0-9a-f]+$/i', '2' => '/^[0-9a-f]+$/i', '3' => '/^OP_2DROP/', '4' => '/^OP_DROP/', '5' => '/^OP_DUP/', '6' => '/^OP_HASH160/', '7' => '/^[0-9a-f]{40}$/i', // pos 7 '8' => '/^OP_EQUALVERIFY/', '9' => '/^OP_CHECKSIG/', ]; // Non-standard (claim_name and support_claim) $define['p2c2'] = array('type' => 'nonstandard', 'reqSigs' => 1, 'data_index_for_hash' => 7); $rule['p2c2'] = [ '0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/', '1' => '/^OP_0/', '2' => '/^[0-9a-f]+$/i', '3' => '/^OP_2DROP/', '4' => '/^OP_DROP/', '5' => '/^OP_DUP/', '6' => '/^OP_HASH160/', '7' => '/^[0-9a-f]{40}$/i', // pos 8 '8' => '/^OP_EQUALVERIFY/', '9' => '/^OP_CHECKSIG/' ]; // update_claim $define['p2uc'] = array('type' => 'nonstandard', 'reqSigs' => 1, 'data_index_for_hash' => 8); $rule['p2uc'] = [ '0' => '/^OP_UPDATE_CLAIM/', '1' => '/^[0-9a-f]+$/i', '2' => '/^[0-9a-f]+$/i', '3' => '/^[0-9a-f]+$/i', '4' => '/^OP_2DROP/', '5' => '/^OP_2DROP/', '6' => '/^OP_DUP/', '7' => '/^OP_HASH160/', '8' => '/^[0-9a-f]{40}$/i', // pos 8 '9' => '/^OP_EQUALVERIFY/', '10' => '/^OP_CHECKSIG/' ]; // Standard: pay to pubkey hash $define['p2ph'] = array('type' => 'pubkeyhash', 'reqSigs' => 1, 'data_index_for_hash' => 2); $rule['p2ph'] = array( '0' => '/^OP_DUP/', '1' => '/^OP_HASH160/', '2' => '/^[0-9a-f]{40}$/i', // 2 '3' => '/^OP_EQUALVERIFY/', '4' => '/^OP_CHECKSIG/'); if ($has_claim) { unset($rule['p2ph']); unset($rule['p2sh']); if ($has_op_0) { unset($rule['p2c']); } else { unset($rule['p2c2']); } if ($has_update_claim) { unset($rule['p2c']); } else { unset($rule['p2uc']); } } else { unset($rule['p2c']); unset($rule['p2c2']); unset($rule['p2uc']); } // Work out how many rules are applied in each case $valid = array(); foreach ($rule as $tx_type => $def) { $valid[$tx_type] = count($def); } // Attempt to validate against each of these rules. $matches = []; for ($index = 0; $index < count($data); $index++) { $test = $data[$index]; foreach ($rule as $tx_type => $def) { if (isset($def[$index])) { preg_match($def[$index], $test, $matches[$tx_type]); if (count($matches[$tx_type]) == 1) { $valid[$tx_type]--; break; } } } } // Loop through rules, check if any transaction is a match. foreach ($rule as $tx_type => $def) { if ($valid[$tx_type] == 0) { // Load predefined info for this transaction type if detected. $return = $define[$tx_type]; if ($tx_type === 'p2pk') { $return['hash160'] = self::hash160($data[$define[$tx_type]['data_index_for_hash']]); $return['addresses'][0] = self::hash160_to_address($return['hash160'], self::pubKeyAddress[0]); } else { $return['hash160'] = $data[$define[$tx_type]['data_index_for_hash']]; $return['addresses'][0] = self::hash160_to_address($return['hash160'], ($tx_type === 'p2sh') ? self::scriptAddress[0] : self::pubKeyAddress[0]); // TODO: Pay to claim transaction? } unset($return['data_index_for_hash']); } } return (!isset($return)) ? false : $return; } public static function _decode_scriptPubKey($script, $matchBitcoinCore = false): string{ $data = array(); while (strlen($script) !== 0) { $byteHex = self::_return_bytes($script, 1); $byteInt = hexdec($byteHex); if (isset(self::$op_code[$byteHex])) { // This checks if the OPCODE is defined from the list of constants. if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_0") { $data[] = '0'; } else if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_1") { $data[] = '1'; } else { $data[] = self::$op_code[$byteHex]; } } elseif ($byteInt >= 0x01 && $byteInt <= 0x4e) { // This checks if the OPCODE falls in the PUSHDATA range if ($byteInt == 0x4d) { // OP_PUSHDATA2 $byteInt = hexdec(self::_return_bytes($script, 2, true)); $data[] = self::_return_bytes($script, $byteInt); } else if ($byteInt == 0x4e) { // OP_PUSHDATA4 $byteInt = hexdec(self::_return_bytes($script, 4, true)); $data[] = self::_return_bytes($script, $byteInt); } else if ($byteInt == 0x4c) { $num_bytes = hexdec(self::_return_bytes($script, 1, true)); $data[] = self::_return_bytes($script, $num_bytes); } else { $data[] = self::_return_bytes($script, $byteInt); } } elseif ($byteInt >= 0x51 && $byteInt <= 0x60) { // This checks if the CODE falls in the OP_X range $data[] = $matchBitcoinCore ? ($byteInt - 0x50) : 'OP_' . ($byteInt - 0x50); } else { throw new \RuntimeException("Failed to decode scriptPubKey"); } } return implode(" ", $data); } /** * Lock * @param $process_name * @return void */ public static function lock($process_name): void{ $lock = Cache::lock($process_name); if(!$lock->get()){ echo $process_name." is already running.\n"; exit(0); } } /** * Unlock * @param $process_name * @return bool */ public static function unlock($process_name): bool{ Cache::lock($process_name)->forceRelease(); return true; } public static function _return_bytes(&$string, $byte_count, $reverse = false): string{ if (strlen($string) < $byte_count * 2) { throw new InvalidArgumentException("Could not read enough bytes"); } $requested_bytes = substr($string, 0, $byte_count * 2); // Overwrite $string, starting $byte_count bytes from the start. $string = substr($string, $byte_count * 2); // Flip byte order if requested. return ($reverse == false) ? $requested_bytes : self::_flip_byte_order($requested_bytes); } public static function _flip_byte_order($bytes): string{ return implode('', array_reverse(str_split($bytes, 2))); } /*public function parsehistoryblocks() { set_time_limit(0); header('Content-type: text/plain'); $block_hash = null; // Get the minimum block hash first $minBlock = $this->Blocks->find()->select(['Hash'])->order(['Height' => 'asc'])->first(); if (!$minBlock) { // get the best block $req = ['method' => 'status','id'=>rand()]; $response = self::curl_json_post(self::lbryurl, json_encode($req)); $json = json_decode($response); $block_hash = $json->result->blockchain_status->best_blockhash; } else { $block_hash = $minBlock->Hash; } echo "Processing block: $block_hash... "; $req = ['method' => 'block_show', 'params' => ['blockhash' => $block_hash],'id'=>rand()]; $response = self::curl_json_post(self::lbryurl, json_encode($req)); $json = json_decode($response); $block_data = $json->result; // Check if the block exists $oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $block_hash])->first(); if (!$oldBlock) { // Block does not exist, create the block $newBlock = $this->blockdb_data_from_json($block_data); $entity = $this->Blocks->newEntity($newBlock); $this->Blocks->save($entity); } echo "Done.\n"; $prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null; do { $oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $prevBlockHash])->first(); $req = ['method' => 'block_show', 'params' => ['blockhash' => $prevBlockHash],'id'=>rand()]; $response = self::curl_json_post(self::lbryurl, json_encode($req)); $json = json_decode($response); $block_data = $json->result; $prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null; if (!$oldBlock) { echo "Inserting block: $block_data->hash... "; $newBlock = $this->blockdb_data_from_json($block_data); $entity = $this->Blocks->newEntity($newBlock); $this->Blocks->save($entity); } else { echo "Updating block: $block_data->hash with confirmations: $block_data->confirmations... "; $updData = ['Id' => $oldBlock->Id, 'Confirmations' => $block_data->confirmations]; $entity = $this->Blocks->newEntity($newBlock); $this->Blocks->save($entity); } echo "Done.\n"; } while($prevBlockHash != null && strlen(trim($prevBlockHash)) > 0); exit(0); } public function updatespends() { set_time_limit(0); self::lock('updatespends'); try { $conn = ConnectionManager::get('default'); $inputs = $this->Inputs->find()->select(['Id', 'PrevoutHash', 'PrevoutN'])->where(['PrevoutSpendUpdated' => 0, 'IsCoinbase <>' => 1])->limit(500000)->toArray(); $count = count($inputs); $idx = 0; echo sprintf("Processing %d inputs.\n", $count); foreach ($inputs as $in) { $idx++; $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT); $tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $in->PrevoutHash])->first(); if ($tx) { $data_error = false; $conn->begin(); try { // update the corresponding output and set it as spent $conn->execute('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ?, Modified = UTC_TIMESTAMP() WHERE TransactionId = ? AND Vout = ?', [$in->Id, $tx->Id, $in->PrevoutN]); } catch (\Exception $e) { $data_error = true; } if (!$data_error) { // update the input $in_data = ['Id' => $in->Id, 'PrevoutSpendUpdated' => 1]; $in_entity = $this->Inputs->newEntity($in_data); $result = $this->Inputs->save($in_entity); if (!$result) { $data_error = true; } } if ($data_error) { echo sprintf("[$idx_str/$count] Could NOT update vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash); $conn->rollback(); } else { echo sprintf("[$idx_str/$count] Updated vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash); $conn->commit(); } } else { echo sprintf("[$idx_str/$count] Transaction NOT found for tx hash %s.\n", $in->PrevoutHash); } } } catch (\Exception $e) { print_r($e); } self::unlock('updatespends'); } */ }