diff --git a/.env.example b/.env.example index a761f166..f41639e6 100644 --- a/.env.example +++ b/.env.example @@ -667,6 +667,72 @@ MODULE_ton-main_NODES[]=http://login:password@127.0.0.2:1234/ MODULE_ton-main_REQUESTER_TIMEOUT=60 MODULE_ton-main_REQUESTER_THREADS=12 +####################### +## Main TRON Module +####################### + +MODULES[]=tron-main +MODULE_tron-main_CLASS=TronMainModule +MODULE_tron-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-main_REQUESTER_TIMEOUT=60 +MODULE_tron-main_REQUESTER_THREADS=2 + +####################### +## Internal TRON Module +####################### + +MODULES[]=tron-internal +MODULE_tron-internal_CLASS=TronInternalModule +MODULE_tron-internal_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-internal_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-internal_REQUESTER_TIMEOUT=60 +MODULE_tron-internal_REQUESTER_THREADS=2 + +####################### +## TRC-10 TRON Module +####################### + +MODULES[]=tron-trc-10 +MODULE_tron-trc-10_CLASS=TronTRC10Module +MODULE_tron-trc-10_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-10_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-10_REQUESTER_TIMEOUT=60 +MODULE_tron-trc-10_REQUESTER_THREADS=2 + +####################### +## TRC-20 TRON Module +####################### + +MODULES[]=tron-trc-20 +MODULE_tron-trc-20_CLASS=TronTRC20Module +MODULE_tron-trc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-20_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-20_REQUESTER_TIMEOUT=60 +MODULE_tron-trc-20_REQUESTER_THREADS=2 + +####################### +## TRC-721 TRON Module +####################### + +MODULES[]=tron-trc-721 +MODULE_tron-trc-721_CLASS=TronTRC721Module +MODULE_tron-trc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-721_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-721_REQUESTER_TIMEOUT=60 +MODULE_tron-trc-721_REQUESTER_THREADS=2 + +####################### +## TRC-1155 TRON Module +####################### + +MODULES[]=tron-trc-1155 +MODULE_tron-trc-1155_CLASS=TronTRC1155Module +MODULE_tron-trc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-1155_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_tron-trc-1155_REQUESTER_TIMEOUT=60 +MODULE_tron-trc-1155REQUESTER_THREADS=2 + #################### ## Zcash Main Module #################### @@ -699,6 +765,7 @@ FRONT_polygon-zkevm_ECOSYSTEM_TITLE=Polygon zkEVM Ecosystem FRONT_optimism_ECOSYSTEM_TITLE=Optimism Ecosystem FRONT_gnosis-chain_ECOSYSTEM_TITLE=Gnosis Chain Ecosystem FRONT_base_ECOSYSTEM_TITLE=Base Ecosystem +FRONT_tron_ECOSYSTEM_TITLE=Tron Ecosystem FRONT_bitcoin_ECOSYSTEM_DESCRIPTION=Includes Bitcoin only FRONT_litecoin_ECOSYSTEM_DESCRIPTION=Includes Litecoin only @@ -717,6 +784,7 @@ FRONT_polygon-zkevm_ECOSYSTEM_DESCRIPTION=Includes Polygon zkEVM only FRONT_optimism_ECOSYSTEM_DESCRIPTION=Includes Optimism only FRONT_gnosis-chain_ECOSYSTEM_DESCRIPTION=Includes Gnosis Chain only FRONT_base_ECOSYSTEM_DESCRIPTION=Includes Base only +FRONT_tron_ECOSYSTEM_DESCRIPTION=Includes Tron only FRONT_bitcoin_BLOCKCHAIN_TITLE=Bitcoin FRONT_litecoin_BLOCKCHAIN_TITLE=Litecoin @@ -735,6 +803,7 @@ FRONT_polygon-zkevm_BLOCKCHAIN_TITLE=Polygon zkEVM FRONT_optimism_BLOCKCHAIN_TITLE=Optimism FRONT_gnosis-chain_BLOCKCHAIN_TITLE=Gnosis Chain FRONT_base_BLOCKCHAIN_TITLE=Base +FRONT_tron_BLOCKCHAIN_TITLE=Tron FRONT_bitcoin_BLOCKCHAIN_DESCRIPTION=The one and only blockchain FRONT_litecoin_BLOCKCHAIN_DESCRIPTION=The digital silver blockchain @@ -753,6 +822,7 @@ FRONT_polygon-zkevm_BLOCKCHAIN_DESCRIPTION=Polygon zkEVM FRONT_optimism_BLOCKCHAIN_DESCRIPTION=Optimism FRONT_gnosis-chain_BLOCKCHAIN_DESCRIPTION=Gnosis Chain FRONT_base_BLOCKCHAIN_DESCRIPTION=Base +FRONT_tron_BLOCKCHAIN_DESCRIPTION=Tron FRONT_bitcoin-main_MODULE_TITLE=Main FRONT_bitcoin-omni_MODULE_TITLE=Omni Layer @@ -808,6 +878,12 @@ FRONT_base-trace_MODULE_TITLE=Internal FRONT_base-erc-20_MODULE_TITLE=ERC-20 FRONT_base-erc-721_MODULE_TITLE=ERC-721 FRONT_base-erc-1155_MODULE_TITLE=ERC-1155 +FRONT_tron-main_MODULE_TITLE=Main +FRONT_tron-internal_MODULE_TITLE=Internal +FRONT_tron-trc-10_MODULE_TITLE=TRC-10 +FRONT_tron-trc-20_MODULE_TITLE=TRC-20 +FRONT_tron-trc-721_MODULE_TITLE=TRC-721 +FRONT_tron-trc-1155_MODULE_TITLE=TRC-1155 FRONT_bitcoin-main_MODULE_DESCRIPTION=Main Bitcoin transfers FRONT_bitcoin-omni_MODULE_DESCRIPTION=Omni Layer transfers @@ -863,6 +939,12 @@ FRONT_base-trace_MODULE_DESCRIPTION=Internal Base transfers (trace) FRONT_base-erc-20_MODULE_DESCRIPTION=ERC-20 token transfers FRONT_base-erc-721_MODULE_DESCRIPTION=ERC-721 token transfers FRONT_base-erc-1155_MODULE_DESCRIPTION=ERC-1155 token transfers +FRONT_tron-main_MODULE_DESCRIPTION=Main Tron transfers +FRONT_tron-internal_MODULE_DESCRIPTION=Internal Tron transfers +FRONT_tron-trc-10_MODULE_DESCRIPTION=TRC-10 token transfers +FRONT_tron-trc-20_MODULE_DESCRIPTION=TRC-20 token transfers +FRONT_tron-trc-721_MODULE_DESCRIPTION=TRC-721 token transfers +FRONT_tron-trc-1155_MODULE_DESCRIPTION=TRC-1155 token transfers ############ # DigiByte # diff --git a/3xpl.php b/3xpl.php index 2c584e46..8dd0851e 100644 --- a/3xpl.php +++ b/3xpl.php @@ -96,6 +96,8 @@ echo N . cli_format_bold('Please select an action: ') . N; echo 'Get latest block number ' . cli_format_reverse('') . ', Process block ' . cli_format_reverse('') . + ', Process back ' . cli_format_reverse('') . + ', Process range ' . cli_format_reverse('') . ', Monitor blockchain ' . cli_format_reverse('') . ', Check handle ' . cli_format_reverse('') . ', Run tests ' . cli_format_reverse('') . @@ -113,7 +115,7 @@ $input_argv[] = $chosen_option; -if (!in_array($chosen_option, ['L', 'B', 'M', 'H', 'T'])) +if (!in_array($chosen_option, ['L', 'B', 'PB', 'PR', 'M', 'H', 'T'])) die(cli_format_error('Wrong choice for 2nd param') . N); echo N; @@ -224,6 +226,7 @@ // address currency sign effect valid extra ?extra_indexed $tsv_fields = ['block', 'transaction', 'sort_key', 'time', 'address', 'currency', 'sign', 'effect', 'valid', 'extra']; + $tsv = ''; foreach ($events as $event) @@ -308,6 +311,104 @@ ddd($output_events); } } +elseif ($chosen_option === 'PB') +{ + echo cli_format_bold('Start block number please...') . N; + + if (isset($argv[3])) + { + $chosen_block_id = (int)$argv[3]; + echo ":> {$chosen_block_id}\n"; + } + else + { + $chosen_block_id = (int)readline(':> '); + } + + $start_block_id = $chosen_block_id > 0 ? $chosen_block_id : $module->inquire_latest_block(); + + if ($start_block_id != $chosen_block_id) + echo cli_format_bold('Processing blocks from latest down to genesis...'); + else + echo cli_format_bold("Processing blocks from {$start_block_id} up to genesis..."); + for ($i = $start_block_id; $i != 0; $i--) + { + echo "\nProcessing block #{$i} "; + + $t0 = microtime(true); + + try + { + $module->process_block($i); + } + catch (RequesterException) + { + echo cli_format_error('Requested exception'); + usleep(250000); + } + + $event_count = count($module->get_return_events() ?? []); + $currency_count = count($module->get_return_currencies() ?? []); + + $time = number_format(microtime(true) - $t0, 4); + + echo "with {$event_count} events and {$currency_count} currencies in {$time} seconds"; + } +} +elseif ($chosen_option === 'PR') +{ + echo cli_format_bold('Start block number please...') . N; + + if (isset($argv[3])) + { + $start_block_id = (int)$argv[3]; + echo ":> {$start_block_id}\n"; + } + else + { + $start_block_id = (int)readline(':> '); + } + echo cli_format_bold('End block number please...') . N; + + if (isset($argv[4])) + { + $end_block_id = (int)$argv[4]; + echo ":> {$end_block_id}\n"; + } + else + { + $end_block_id = (int)readline(':> '); + } + echo N; + + echo cli_format_bold('Processing range of blocks...'); + $increment = $start_block_id > $end_block_id ? -1 : 1; + for ($i = $start_block_id; $i != $end_block_id; $i=$i+$increment) + { + echo "\nProcessing block #{$i} "; + + $t0 = microtime(true); + + try + { + $module->process_block($i); + } + catch (RequesterException) + { + echo cli_format_error('Requested exception'); + usleep(250000); + } + + $event_count = count($module->get_return_events() ?? []); + $currency_count = count($module->get_return_currencies() ?? []); + + $time = number_format(microtime(true) - $t0, 4); + + echo "with {$event_count} events and {$currency_count} currencies in {$time} seconds"; + } + +} + elseif ($chosen_option === 'M') { $best_known_block = $module->inquire_latest_block(); diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f612575e..7c87c9e5 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -5,5 +5,7 @@ - Core, UTXO modules, EVM modules * [Yulian Volianskyi](https://github.com/jzethar) - Beacon Chain modules +* [alexqrid](https://github.com/alexqrid) + - TVM modules * [Oleg Makaussov](https://github.com/Lorgansar) - - Cardano Tokens modules \ No newline at end of file + - Cardano Tokens modules diff --git a/Engine/Crypto/Base58.php b/Engine/Crypto/Base58.php new file mode 100644 index 00000000..bbb1973a --- /dev/null +++ b/Engine/Crypto/Base58.php @@ -0,0 +1,138 @@ += 58) + { + $div = bcdiv($decimal, '58'); + $mod = bcmod($decimal, '58'); + $output .= self::ALPHABET[(int)$mod]; + $decimal = $div; + } + + if ($decimal > 0) + { + $output .= self::ALPHABET[$decimal]; + } + + $output = strrev($output); + + foreach ($bytes as $byte) + { + if ($byte === 0) + { + $output = self::ALPHABET[0] . $output; + continue; + } + break; + } + + return $output; + } + + private static function base58_decode($base58) + { + if (!$base58) + return ''; + + $indexes = array_flip(str_split(self::ALPHABET)); + $chars = str_split($base58); + + foreach ($chars as $char) + if (isset($indexes[$char]) === false) + return false; + + $decimal = $indexes[$chars[0]]; + + for ($i = 1, $l = count($chars); $i < $l; $i++) + { + $decimal = bcmul($decimal, '58'); + $decimal = bcadd($decimal, $indexes[$chars[$i]]); + } + + $output = ''; + + while ($decimal > 0) + { + $byte = bcmod($decimal, '256'); + $output = pack('C', $byte) . $output; + $decimal = bcdiv($decimal, '256'); + } + + foreach ($chars as $char) + { + if ($indexes[$char] === 0) + { + $output = '\x00' . $output; + continue; + } + + break; + } + + return $output; + } + + private static function base58_check_encode($address) + { + $hash0 = hash('sha256', $address); + $hash1 = hash('sha256', hex2bin($hash0)); + $checksum = substr($hash1, 0, 8); + $address = $address . hex2bin($checksum); + $base58add = self::base58_encode($address); + + return $base58add; + } + + private static function base58_check_decode($base58add) + { + $address = self::base58_decode($base58add); + $size = strlen($address); + + if ($size != 25) + return false; + + $checksum = substr($address, 21); + $address = substr($address, 0, 21); + $hash0 = hash('sha256', $address); + $hash1 = hash('sha256', hex2bin($hash0)); + $checksum0 = substr($hash1, 0, 8); + $checksum1 = bin2hex($checksum); + + if (strcmp($checksum0, $checksum1)) + return false; + + return $address; + } + + public static function hex_to_base58_check($hex_string) + { + return self::base58_check_encode(hex2bin($hex_string)); + } + + public static function base58_check_to_hex($base58) + { + return bin2hex(self::base58_check_decode($base58)); + } +} diff --git a/Engine/Exceptions.php b/Engine/Exceptions.php index 8a2b1014..1e35e275 100644 --- a/Engine/Exceptions.php +++ b/Engine/Exceptions.php @@ -15,6 +15,7 @@ class ModuleError extends Error {} // This should be used by module developers i class RequesterException extends Exception {} // Curl errors class RequesterEmptyResponseException extends RequesterException {} // This can be caught if an empty response is considered to be a valid response +class RequesterEmptyArrayInResponseException extends RequesterException {} // This can be caught if an empty array response is considered to be a valid response class MathException extends Exception {} // This is for math exceptions class ConsensusException extends Exception {} // This is a special exception that should be used in ensure_block() in case // if different nodes return different block data diff --git a/Engine/Helpers.php b/Engine/Helpers.php index 84e13fd9..84363472 100644 --- a/Engine/Helpers.php +++ b/Engine/Helpers.php @@ -183,3 +183,17 @@ function remove_passwords($url) $url = parse_url($url); return ($url['scheme'] ?? '').'://'.($url['host'] ?? '').($url['path'] ?? '').($url['query'] ?? ''); } + +// Returns standard unixtime +function to_timestamp_from_long_unixtime(string $long_unixtime): string +{ + // 1555400628000 + return DateTime::createFromFormat('U.u', bcdiv($long_unixtime, '1000', 3))->format("Y-m-d H:i:s"); +} + +function remove_0x_safely(string $string): string +{ + if (substr($string, 0, 2) !== '0x') + throw new DeveloperError("remove_0x_safely({$string}): missing 0x"); + return substr($string, 2); +} diff --git a/Engine/Requester.php b/Engine/Requester.php index 3fb8b2cc..500c3f89 100644 --- a/Engine/Requester.php +++ b/Engine/Requester.php @@ -75,13 +75,14 @@ function requester_single($daemon, $endpoint = '', $params = [], $result_in = '' } curl_close($curl); - if (is_null($output)) throw new RequesterException("requester_request(daemon:({$daemon_clean}), endpoint:({$endpoint}), params:({$params_log}), result_in:({$result_in})) failed: output is `null`"); if ($output === '') throw new RequesterEmptyResponseException("requester_request(daemon:({$daemon_clean}), endpoint:({$endpoint}), params:({$params_log}), result_in:({$result_in})) failed: output is an empty string"); if ($output === false) throw new RequesterException("requester_request(daemon:({$daemon_clean}), endpoint:({$endpoint}), params:({$params_log}), result_in:({$result_in})) failed: output is false (timeout?)"); + if (trim($output) === '{}' || trim($output) === '[]') + throw new RequesterEmptyArrayInResponseException("requester_request(daemon:({$daemon_clean}), endpoint:({$endpoint}), params:({$params_log}), result_in:({$result_in})) failed: output is an empty array"); // Here we add quotes to all numeric values not to lose precision if some are larger than int64. // Note that this doesn't work good with values like `2.5e-8`, so there's the IgnoreAddingQuotesToNumbers option diff --git a/Modules/Common/CoreModule.php b/Modules/Common/CoreModule.php index b015aa3b..34597bf0 100644 --- a/Modules/Common/CoreModule.php +++ b/Modules/Common/CoreModule.php @@ -151,8 +151,8 @@ public function __construct() // Nodes - $this->nodes = envm($this->module, 'NODES', new DeveloperError('Nodes are not set in the config')); - $this->timeout = envm($this->module, 'REQUESTER_TIMEOUT', new DeveloperError('Timeout is not set in the config')); + $this->nodes = envm($this->module, 'NODES', new DeveloperError("Nodes are not set in the config for module {$this->module}")); + $this->timeout = envm($this->module, 'REQUESTER_TIMEOUT', new DeveloperError("Timeout is not set in the config for module {$this->module}")); // Post-initialization. Here we check if all settings are applied correctly. diff --git a/Modules/Common/TVMInternalModule.php b/Modules/Common/TVMInternalModule.php new file mode 100644 index 00000000..8fe88982 --- /dev/null +++ b/Modules/Common/TVMInternalModule.php @@ -0,0 +1,99 @@ +version = 1; + } + + final public function post_post_initialize() + { + + } + + final public function pre_process_block($block_id) + { + try { + $r1 = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}", + timeout: $this->timeout); // example block_num 21575018 + } catch (RequesterEmptyArrayInResponseException) { + $r1 = []; + } + + $events = []; + $sort_key = 0; + foreach ($r1 as $transaction) { + if (!isset($transaction['internal_transactions'])) + continue; + foreach ($transaction['internal_transactions'] as $internal_data) { + foreach ($internal_data['callValueInfo'] as $data) { + if (isset($data['callValue']) && !isset($data['tokenId'])) { + $events[] = [ + 'transaction' => $transaction['id'], + 'address' => $this->encode_address_to_base58('0x' . substr($internal_data['caller_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['callValue'], + 'failed' => $internal_data['rejected'] ?? false + ]; + + $events[] = [ + 'transaction' => $transaction['id'], + 'address' => $this->encode_address_to_base58('0x' . substr($internal_data['transferTo_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => strval($data['callValue']), + 'failed' => $internal_data['rejected'] ?? false + ]; + } + } + } + } + + //////////////// + // Processing // + //////////////// + + foreach ($events as &$event) { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + } +} diff --git a/Modules/Common/TVMMainModule.php b/Modules/Common/TVMMainModule.php new file mode 100644 index 00000000..e849834b --- /dev/null +++ b/Modules/Common/TVMMainModule.php @@ -0,0 +1,652 @@ +version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->currency)) + throw new DeveloperError("`currency` is not set (developer error)"); + + if (is_null($this->extra_data_details)) + throw new DeveloperError("`extra_data_details` is not set (developer error)"); + + if (is_null($this->reward_function)) + throw new DeveloperError("`reward_function` is not set (developer error)"); + } + + final public function pre_process_block($block_id) + { + //////////////////////////////// + // Getting data from the node // + //////////////////////////////// + + $transaction_data = []; + + if ($block_id !== MEMPOOL) + { + $r1 = requester_single($this->select_node(), + endpoint: "/wallet/getblockbynum?num={$block_id}&visible=true", + timeout: $this->timeout); + + $r2 = requester_single($this->select_node(), + endpoint: "/", + params: ['method' => 'eth_getBlockByNumber', + 'params' => [to_0xhex_from_int64($block_id), true], + 'id' => 0, + 'jsonrpc' => '2.0', + ], result_in: 'result', timeout: $this->timeout); + + try + { + $receipt_data = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}&visible=true", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $receipt_data = []; + } + + $general_data = $r1['transactions'] ?? []; + + // we can't get 'from','to' the other way + // when we don't know params of specific system contract + // but evm like jsonrpc response is the way + $evm_transaction_data = $r2['transactions']; + + $this->block_time = to_timestamp_from_long_unixtime($r1['block_header']['raw_data']['timestamp'] ?? '1529891469000'); + + $miner = $r1['block_header']['raw_data']['witness_address']; + + // Data processing + // $receipt_data can be empty + if ((($ic = count($general_data)) !== count($receipt_data)) && ($ic !== count($evm_transaction_data))) { + throw new ModuleError('Mismatch in transaction count'); + } + for ($i = 0; $i < $ic; $i++) + { + if ((count($receipt_data) > 0) && ($general_data[$i]['txID'] !== $receipt_data[$i]['id']) && ($general_data[$i]['txID'] != substr($evm_transaction_data[$i]['hash'], 2))) + { + throw new ModuleError('Mismatch in transaction order'); + } + + if (!isset($general_data[$i]['raw_data']['contract'][0]['parameter']['value'])) + { + throw new ModuleError("Error in transaction {$general_data[$i]['txID']} data: no raw_data.contract.parameter.value"); + } + if (count($general_data[$i]['raw_data']['contract']) > 1) + { + throw new ModuleError("Error in transaction {$general_data[$i]['txID']} data: more than 1 raw_data.contract element"); + } + + if (isset($general_data[$i]['ret']) && count($general_data[$i]['ret']) > 1) // found in block 9972983 + { + foreach ($general_data[$i]['ret'] as $ret) + { + if (isset($ret["contractRet"])) + { + $general_data[$i]['ret'] = $ret; + break; + } + } + } + + if (!isset($general_data[$i]['raw_data']['contract'][0]['type'])) + { + throw new ModuleError("Contract interaction type missing in {$general_data[$i]['txID']}."); + } + + $data = $general_data[$i]['raw_data']['contract'][0]['parameter']['value']; + $transaction_type = $general_data[$i]['raw_data']['contract'][0]['type']; + + if (!in_array($transaction_type, $this->extra_data_details)) { + throw new ModuleError("Unknown contract interaction type {$transaction_type} in {$general_data[$i]['txID']}."); + } + + switch ($transaction_type) { + case "AccountCreateContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['account_address'], + 'value' => 0, + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + + ]; + break; + case "TransferContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['to_address'], + 'value' => $data['amount'] ?? 0, + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "VoteWitnessContract": + case "WitnessUpdateContract": + case "AccountUpdateContract": + case "ProposalCreateContract": + case "ProposalApproveContract": + case "ProposalDeleteContract": + case "MarketSellAssetContract": + case "MarketCancelOrderContract": + case "SetAccountIdContract": + case "AccountPermissionUpdateContract": + case "UpdateBrokerageContract": + case "WitnessCreateContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => null, + 'value' => 0, + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "FreezeBalanceContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['receiver_address'] ?? null, + 'value' => $data['frozen_balance'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "UnfreezeBalanceContract": + $from = $data['receiver_address'] ?? null; + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => null, // owner_address shouldn't be put here as he staked balance previously + 'to' => is_null($from) ? $data['owner_address'] : $data['receiver_address'], + 'value' => $receipt_data[$i]['unfreeze_amount'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "WithdrawBalanceContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => 'treasury', + 'to' => $data['owner_address'], + 'value' => $receipt_data[$i]["withdraw_amount"], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "CreateSmartContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => null, + 'value' => 0, + 'contractAddress' => $receipt_data[$i]['contract_address'], + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "UpdateEnergyLimitContract": + case "ClearABIContract": + case "UpdateSettingContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['contract_address'], + 'value' => 0, + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "FreezeBalanceV2Contract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => null, + 'value' => $data['frozen_balance'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "UnfreezeBalanceV2Contract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => null, + 'to' => $data['owner_address'], + 'value' => $data['unfreeze_balance'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "WithdrawExpireUnfreezeContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => null, + 'to' => $data['owner_address'], + 'value' => $receipt_data[$i]['withdraw_expire_amount'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "DelegateResourceContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['receiver_address'], + 'value' => $data['balance'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "UndelegateResourceContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['receiver_address'], + 'to' => $data['owner_address'], + 'value' => $data['balance'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + case "CancelAllUnfreezeV2Contract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => null, + 'to' => $data['owner_address'], + 'value' => to_int256_from_0xhex($r2['transactions'][$i]['value']), + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + // Exchanges + case "ExchangeInjectContract": + if ($data['token_id'] == "_") { + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => 'dex', + 'value' => $data['quant'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; // only inside if, because we need to process the fee if this is not a trx dex transfer + } + goto fee_process; // don't iterate over other cases just go to default case + case "ExchangeTransactionContract": + $exchange = $this->get_exchange_by_id($data['exchange_id']); + if ($exchange['has_trx'] && $data['token_id'] != "_") // buying trx for token 4067933 + { + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => 'dex', + 'to' => $data['owner_address'], + 'value' => $receipt_data[$i]['exchange_received_amount'] ?? 0, // for failed transaction it can be unset not sure + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; // only inside if, because we need to process the fee if this is not a trx dex transfer + } + elseif ($exchange['has_trx'] && $data['token_id'] === "_") // buying token for trx + { + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => 'dex', + 'value' => $data['quant'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; // only inside if, because we need to process the fee if this is not a trx dex transfer + } + goto fee_process; + case "ExchangeWithdrawContract": + if ($data['token_id'] == "_") { + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => 'dex', + 'to' => $data['owner_address'], + 'value' => $data['quant'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; // only inside if, because we need to process the fee if this is not a trx dex transfer + } + goto fee_process; // fail case in 5969073 + case "ExchangeCreateContract": + if (($data['first_token_id'] == "_") || ($data['second_token_id'] == "_")) + $value = $data['first_token_id'] == '_' ? $data['first_token_balance'] : ($data['second_token_id'] == "_" ? $data['second_token_balance'] : null); + if (!is_null($value ?? null)) + { + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => 'dex', + 'value' => $value, + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; // only inside if, because we need to process the fee if this is not a trx dex transfer + } + goto fee_process; + case "ParticipateAssetIssueContract": + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $data['owner_address'], + 'to' => $data['to_address'] ?? null, + 'value' => $data['amount'], + 'contractAddress' => null, + 'fee' => $receipt_data[$i]["fee"] ?? 0, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + + // unknown processing rules/default case + case "ShieldedTransferContract": + case "CustomContract": + case "GetContract": + default: + fee_process: + $from = $this->encode_address_to_base58($evm_transaction_data[$i]['from']); + $to = $this->encode_address_to_base58($evm_transaction_data[$i]['to']); + $value = to_int256_from_0xhex($evm_transaction_data[$i]['value']); + if ($transaction_type === "TriggerSmartContract") + $value = 0; // exclude double transfer, as it will be processed in internal module + // ShieldedTransferContract has additional fee in `shielded_transaction_fee` key + $fee = ($receipt_data[$i]["fee"] ?? 0) + ($receipt_data[$i]['shielded_transaction_fee'] ?? 0); + $transaction_data[($general_data[$i]['txID'])] = + [ + 'from' => $from, + 'to' => $to, + 'value' => $value, + 'contractAddress' => null, + 'fee' => $fee, + 'status' => ($general_data[$i]['ret'][0]['contractRet'] ?? "SUCCESS") != "SUCCESS", + 'extra' => TVMSpecialTransactions::fromName($transaction_type) + ]; + break; + } + + } + } +// else // Mempool processing +// { +// // wallet/getpendingsize +// // wallet/gettransactionlistfrompending only list of transaction ids +// // wallet/gettransactionfrompending single tx info +// // Mempool transactions are validated very fast +// } + + // check for missing transactions + $processed = count(array_keys($transaction_data)); + $transaction_count = count(array_column($general_data,'txID')); + if ($processed != $transaction_count ) + throw new ModuleError("Some transactions left unprocessed: {$processed}/{$transaction_count}"); + + + ////////////////////// + // Preparing events // + ////////////////////// + + $events = []; + $ijk = 0; + + foreach ($transaction_data as $transaction_hash => $transaction) { + if ($block_id !== MEMPOOL) { + // all fees are burned + $this_burned = strval($transaction['fee']); + } else { + $this_burned = '0'; + } + + // Burning + if ($this_burned !== '0') { + $events[] = [ + 'transaction' => $transaction_hash, + 'address' => $transaction['from'] ?? $transaction['to'], + 'sort_in_block' => $ijk, + 'sort_in_transaction' => 0, + 'effect' => '-' . $this_burned, + 'failed' => false, + 'extra' => TVMSpecialTransactions::Burning->value, + ]; + + $events[] = [ + 'transaction' => $transaction_hash, + 'address' => 'the-void', + 'sort_in_block' => $ijk, + 'sort_in_transaction' => 1, + 'effect' => $this_burned, + 'failed' => false, + 'extra' => TVMSpecialTransactions::Burning->value, + ]; + } + + // The action itself + $val = $transaction['value'] < 0 ? (int)(substr($transaction['value'],1)): $transaction['value']; + $events[] = [ + 'transaction' => $transaction_hash, + 'address' => $transaction['from'] ?? 'the-void', + 'sort_in_block' => $ijk, + 'sort_in_transaction' => 4, + 'effect' => '-' . $val, + 'failed' => $transaction['status'], + 'extra' => $transaction['extra'], + ]; + + if (in_array(TVMSpecialFeatures::AllowEmptyRecipient, $this->extra_features)) + $recipient = $transaction['to'] ?? $transaction['contractAddress'] ?? 'the-void'; + else + $recipient = $transaction['to'] ?? $transaction['contractAddress'] ?? throw new DeveloperError("No address {$transaction_hash}"); + $events[] = [ + 'transaction' => $transaction_hash, + 'address' => $recipient, + 'sort_in_block' => $ijk++, + 'sort_in_transaction' => 5, + 'effect' => strval($val), + 'failed' => $transaction['status'], + 'extra' => $transaction['extra'] + , + ]; + } + + if ($block_id !== MEMPOOL) { + // ToDo get these values from the node + // but nodes are not aware of old values + // /wallet/getchainparameters + // $this_to_miner = response['chainParameter'][$k]['getWitnessPayPerBlock'] + // $this_to_votersresponse['chainParameter'][$k+$n]['getWitness127PayPerBlock'] + // proposal #5 applied on 2019-11-05 + [$this_to_miner, $this_to_voters] = ($this->reward_function)($block_id); + + // SR reward + + $events[] = [ + 'transaction' => null, + 'address' => 'the-void', + 'sort_in_block' => $ijk, + 'sort_in_transaction' => 0, + 'effect' => '-' . $this_to_miner, + 'failed' => false, + 'extra' => TVMSpecialTransactions::BlockReward->value, + ]; + + $events[] = [ + 'transaction' => null, + 'address' => $miner, + 'sort_in_block' => $ijk++, + 'sort_in_transaction' => 1, + 'effect' => $this_to_miner, + 'failed' => false, + 'extra' => TVMSpecialTransactions::BlockReward->value, + ]; + + // Voters rewards (SR partners - 100 voters) + + $events[] = [ + 'transaction' => null, + 'address' => 'the-void', + 'sort_in_block' => $ijk++, + 'sort_in_transaction' => 1, + 'effect' => '-' . $this_to_voters, + 'failed' => false, + 'extra' => TVMSpecialTransactions::PartnerReward->value, + ]; + + $events[] = [ + 'transaction' => null, + 'address' => 'treasury', + 'sort_in_block' => $ijk++, + 'sort_in_transaction' => 1, + 'effect' => $this_to_voters, + 'failed' => false, + 'extra' => TVMSpecialTransactions::PartnerReward->value, + ]; + } + + //////////////// + // Processing // + //////////////// + + $this_time = date('Y-m-d H:i:s'); + foreach ($events as &$event) { + $event['block'] = $block_id; + $event['time'] = ($block_id !== MEMPOOL) ? $this->block_time : $this_time; + } + // Resort + + if ($block_id !== MEMPOOL) { + usort($events, function ($a, $b) { + return [$a['sort_in_block'], + $a['sort_in_transaction'], + ] + <=> + [$b['sort_in_block'], + $b['sort_in_transaction'], + ]; + }); + } + + $sort_key = 0; + + $this_transaction = ''; + + foreach ($events as &$event) { + if ($block_id === MEMPOOL) { + if ($this_transaction != $event['transaction']) { + $this_transaction = $event['transaction']; + $sort_key = 0; + } + } + + $event['sort_key'] = $sort_key; + $sort_key++; + + unset($event['sort_in_block']); + unset($event['sort_in_transaction']); + } + + $this->set_return_events($events); + } + + // Getting balances from the node + public function api_get_balance($address): ?string + { + // assuming that address received in base58 format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // should always be the case + try { + $address = '0x' . $this->encode_base58_to_evm_hex($address); + } catch (Exception) { + return '0'; + } + + // let's keep this extra check + if (!preg_match(StandardPatterns::iHexWith0x40->value, $address)) + return '0'; + + return to_int256_from_0xhex(requester_single($this->select_node(), + endpoint: "/", + params: ['jsonrpc' => '2.0', 'method' => 'eth_getBalance', 'params' => [$address, 'latest'], 'id' => 0], + result_in: 'result', timeout: $this->timeout)); + } +} diff --git a/Modules/Common/TVMTRC10Module.php b/Modules/Common/TVMTRC10Module.php new file mode 100644 index 00000000..8aa5410d --- /dev/null +++ b/Modules/Common/TVMTRC10Module.php @@ -0,0 +1,578 @@ +version = 1; + + } + + final public function post_post_initialize() + { + + } + + final public function pre_process_block($block_id) + { + + try + { + $r1 = requester_single($this->select_node(), + endpoint: "/wallet/getblockbynum?num={$block_id}", // no visible=true, because asset_name can be + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $r1 = []; + } + + $general_data = $r1['transactions'] ?? []; + + try + { + // there can be TRC-10 transfers in internal transactions as well + $r2 = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $r2 = []; + } + + // Process logs + $events = []; + $currencies_to_process = []; + $sort_key = 0; + $exchange_transaction_types = ['ExchangeCreateContract', 'ExchangeInjectContract', 'ExchangeWithdrawContract', 'ExchangeTransactionContract', 'ParticipateAssetIssueContract']; + $other_trc10_transactions_data = []; + $other_trc10_transactions_info = []; + + // main trc10 transfers + for ($i = 0; $i < count($general_data); $i++) + { + $receipt = $general_data[$i]; + + $transaction_type = $receipt['raw_data']['contract'][0]['type'] ?? null; + if (in_array($transaction_type, $exchange_transaction_types)) + { + $other_trc10_transactions_data[] = $receipt; + $other_trc10_transactions_info[] = $r2[$i] ?? []; + continue; + } + + if (count($other_trc10_transactions_data) > 0 && count($r2) === 0) + throw new ModuleError("No transaction info for dex trc10 transfers"); + + if (($transaction_type) !== 'TransferAssetContract') + continue; + + if (($receipt['raw_data']['contract'][0]['type'] ?? null) !== 'TransferAssetContract') + continue; + + $data = $receipt['raw_data']['contract'][0]['parameter']['value']; + $asset_id = $this->get_asset_info($data['asset_name'], block_id: $block_id); + $events[] = [ + 'transaction' => $receipt['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['amount'], + ]; + + $events[] = [ + 'transaction' => $receipt['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['to_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => strval($data['amount']), + ]; + + $currencies_to_process[] = $asset_id; + } + + // process internal trc10 internal transfers + foreach ($r2 as $transaction) + { + if (!isset($transaction['internal_transactions'])) + continue; + foreach ($transaction['internal_transactions'] as $internal_data) + { + foreach ($internal_data['callValueInfo'] as $data) + { + if (isset($data['tokenId']) && isset($data['callValue'])) + { + $asset_id = $this->get_asset_info($data['tokenId'], block_id: $block_id); + $events[] = [ + 'transaction' => $transaction['id'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($internal_data['caller_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['callValue'], + ]; + + $events[] = [ + 'transaction' => $transaction['id'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($internal_data['transferTo_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => strval($data['callValue']), + ]; + $currencies_to_process[] = $asset_id; + } + } + } + } + + // other specific trc10 transfers && asset issue transfers + for ($i = 0; $i < count($other_trc10_transactions_data); $i++) + { + $transaction_type = $other_trc10_transactions_data[$i]['raw_data']['contract'][0]['type'] ?? null; + $data = $other_trc10_transactions_data[$i]['raw_data']['contract'][0]['parameter']['value']; + switch ($transaction_type) + { + case "ExchangeInjectContract": + if ($data['token_id'] != "5f") // '5f' because api queried without `visible=true` query arg, and '_' === '5f' in hex + { + $asset_id = $this->get_asset_info($data['token_id'], block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['quant'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => $data['quant'], + ]; + $currencies_to_process[] = $asset_id; + } + break; + case "ExchangeTransactionContract": + $exchange = $this->get_exchange_by_id($data['exchange_id']); + if ($exchange['has_trx'] && $data['token_id'] != "5f") // buying trx for token, example block 4067933 + { + $asset_id = $this->get_asset_info($data['token_id'], block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['quant'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => strval($data['quant']), + ]; + + $currencies_to_process[] = $asset_id; + } + elseif ($exchange['has_trx'] && $data['token_id'] === "5f") // buying token for trx + { + $asset_id = $exchange['first_token_id'] != '5f' ? $exchange['first_token_id'] : $exchange['second_token_id']; + $asset_id = $this->get_asset_info($asset_id, block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => $other_trc10_transactions_info[$i]['exchange_received_amount'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => '-' . $other_trc10_transactions_info[$i]['exchange_received_amount'], + ]; + + $currencies_to_process[] = $asset_id; + } + elseif ($data['token_id'] != '5f') // exchanging one token to another + { + $received_asset_id = $exchange['first_token_id'] != $data['token_id'] ? $exchange['first_token_id'] : $exchange['second_token_id']; + $received_asset_id = $this->get_asset_info($received_asset_id, block_id: $block_id); + $sold_asset_id = $this->get_asset_info($data['token_id'], block_id: $block_id); + $sold_value = $data['quant']; + $received_value = $other_trc10_transactions_info[$i]['exchange_received_amount']; + + // sold + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $sold_asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $sold_value, + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $sold_asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => $sold_value, + ]; + + // received + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $received_asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => $received_value, + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $received_asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => '-' . $received_value, + ]; + + $currencies_to_process[] = $received_asset_id; + $currencies_to_process[] = $sold_asset_id; + } + break; + case "ExchangeWithdrawContract": + if ($data['token_id'] != "5f") + { + $asset_id = $this->get_asset_info($data['token_id'], block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => $data['quant'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['quant'], + ]; + $currencies_to_process[] = $asset_id; + } + break; + case "ExchangeCreateContract": + if ($data['first_token_id'] != "5f") + { + $asset_id = $this->get_asset_info($data['first_token_id'], block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['first_token_balance'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => $data['first_token_balance'], + ]; + $currencies_to_process[] = $asset_id; + } + + if ($data['second_token_id'] != "5f") + { + $asset_id = $this->get_asset_info($data['second_token_id'], block_id: $block_id); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => '-' . $data['second_token_balance'], + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id, + 'address' => 'dex', + 'sort_key' => $sort_key++, + 'effect' => $data['second_token_balance'], + ]; + $currencies_to_process[] = $asset_id; + } + break; + + case "ParticipateAssetIssueContract": + $asset_id = $this->get_asset_info($data['asset_name'], block_id: $block_id, id_only: false); + $received_value = (int)($data['amount'] * $asset_id['num'] / $asset_id['trx_num']); + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id['id'], + 'address' => $this->encode_address_to_base58('0x' . substr($data['owner_address'], 2)), + 'sort_key' => $sort_key++, + 'effect' => strval($received_value), + ]; + $events[] = [ + 'transaction' => $other_trc10_transactions_data[$i]['txID'], + 'currency' => $asset_id['id'], + 'address' => 'the-void', + 'sort_key' => $sort_key++, + 'effect' => '-' . $received_value, + ]; + $currencies_to_process[] = $asset_id['id']; + break; + } + } + + // Process currencies + + $currencies = []; + $currencies_to_process = array_values(array_unique($currencies_to_process)); // Removing duplicates + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); // Removes already known currencies + + if ($currencies_to_process) + { + $assets = requester_single($this->select_node(), + endpoint: "/wallet/getassetissuelist", + timeout: $this->timeout); + + if (count($assets) < 1) + throw new ModuleError('No results for /wallet/assetissuelist'); + + $assets = $assets['assetIssue']; + + $processed_currencies = []; + + foreach ($assets as $asset) + { + if (in_array($asset['id'], $currencies_to_process)) + $processed_currencies[] = $asset; + } + + foreach ($processed_currencies as $asset) + { + $asset['precision'] = isset($asset['precision']) ? $asset['precision'] : 1; + $asset['abbr'] = $asset['abbr'] ?? ''; + $currencies[] = [ + 'id' => $asset['id'], + 'name' => mb_convert_encoding(hex2bin($asset['name']), 'UTF-8', 'UTF-8'), + 'symbol' => mb_convert_encoding(hex2bin($asset['abbr']), 'UTF-8', 'UTF-8'), + 'decimals' => $asset['precision'] > 32767 ? 1 : $asset['precision'] // We use SMALLINT for decimals... + ]; + } + } + + //////////////// + // Processing // + //////////////// + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } + + // Getting balances from the node + function api_get_balance(string $address, array $currencies): array + { + // assuming that address received in base58 format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // should always be the case + + if (!$currencies) + return []; + + // let's check that address is really in Base58Check format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // this helps to not exhaust the node + try + { + $address = '41' . $this->encode_base58_to_evm_hex($address); + } + catch (Exception) + { + $return = []; + + foreach ($currencies as $ignored) + $return[] = '0'; + return $return; + } + + $real_currencies = []; + + // Input currencies should be in format like this: `tron-trc-10/1000001` + foreach ($currencies as $c) + $real_currencies[] = explode('/', $c)[1]; + + try + { + $data = requester_single($this->select_node(), + endpoint: "/wallet/getaccount?address={$address}", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException){ + return []; + } + + + // i.e. address not found + if (count($data) == 0) + return []; + + $data = $data['assetV2']; + + $result = []; + foreach ($data as $asset) + { + if (in_array($asset['key'], $real_currencies)) + $result[$asset['key']] = $asset['value']; + } + + $return = []; + for ($i = 0, $ids = count($real_currencies); $i < $ids; $i++) + { + $return[] = strval($result[$real_currencies[$i]] ?? 0); + } + return $return; + } + + /** + * The No.14 Committee Proposal + * allows duplicate token name, therefore, before the proposal takes effect, the token name is used as the unique identifier of the TRC10 token. + * After it takes effect, the token id will be used as the unique identifier of the TRC10 token. + * @param string $asset_name_or_id + * @param bool $id_only - if true will return only token ID + * @param int $block_id - depending on block number an appropriate api call will be made (see Proposal 14) + * @return string|array + * @throws RequesterEmptyResponseException + * @throws RequesterException + */ + protected function get_asset_info(string $asset_name_or_id, int $block_id, bool $id_only = true): string|array + { + if (array_key_exists($asset_name_or_id, $this->trc10_tokens)) + { + $result = $this->trc10_tokens[$asset_name_or_id]; + } + else + { + if ($block_id > 5537806) + { + if (strlen($asset_name_or_id) % 2 ==0) + $asset = hex2bin($asset_name_or_id); + else + $asset = $asset_name_or_id; + try + { + $asset_by_id = requester_single($this->select_node(), + endpoint: "/wallet/getassetissuebyid?value=$asset", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $asset_by_id = requester_single($this->select_node(), + endpoint: "/wallet/getassetissuebyid?value=$asset_name_or_id", + timeout: $this->timeout); + } + if (count($asset_by_id) != 0) + $result = [ + 'id' => $asset_by_id['id'], + 'name' => hex2bin($asset_by_id['name']), + 'symbol' => hex2bin($asset_by_id['abbr'] ?? ''), + 'decimals' => $asset_by_id['precision'] ?? 1, + 'num' => $asset_by_id['num'] ?? 1, + 'trx_num' => $asset_by_id['trx_num'] ?? 1 + ]; + else + throw new DeveloperError(" Could not get id of asset after Prop 14 {$asset_name_or_id}: $asset_by_id"); + } + else + { + $asset_by_id = null; + try { + $asset_by_name = requester_single($this->select_node(), + endpoint: "/wallet/getassetissuelistbyname?value=$asset_name_or_id", + result_in: 'assetIssue', timeout: $this->timeout); + }catch (RequesterEmptyArrayInResponseException){ + // unpredicted behaviour in block 5535307 token_id was the number instead of symbol + $asset_by_id = requester_single($this->select_node(), + endpoint: "/wallet/getassetissuebyid?value=" . hex2bin($asset_name_or_id), timeout: $this->timeout); + } + + if (is_null($asset_by_id)) + { + if (!isset($asset_by_name) || count($asset_by_name) < 1) + throw new DeveloperError(" Could not get id of asset {$asset_name_or_id}: $asset_by_name"); + // if the $asset_name_or_id is name of the token, then this was the firstly created token + usort($asset_by_name, fn($a, $b) => (int)$a['id'] <=> (int)$b['id']); + $result = [ + 'id' => $asset_by_name[0]['id'], + 'name' => $asset_by_name[0]['name'], + 'symbol' => $asset_by_name[0]['abbr'] ?? '', + 'decimals' => $asset_by_name[0]['precision'] ?? 1, + 'num' => $asset_by_name[0]['num'] ?? 1, + 'trx_num' => $asset_by_name[0]['trx_num'] ?? 1 + ]; + } + else + { + $result = [ + 'id' => $asset_by_id['id'], + 'name' => hex2bin($asset_by_id['name']), + 'symbol' => hex2bin($asset_by_id['abbr'] ?? ''), + 'decimals' => $asset_by_id['precision'] ?? 1, + 'num' => $asset_by_id['num'] ?? 1, + 'trx_num' => $asset_by_id['trx_num'] ?? 1 + ]; + } + + } + + // populate cache + $this->trc10_tokens[$asset_name_or_id] = $result; + } + + if ($id_only) + return $result['id']; + return $result; + } + +} diff --git a/Modules/Common/TVMTRC1155Module.php b/Modules/Common/TVMTRC1155Module.php new file mode 100644 index 00000000..f2e596ed --- /dev/null +++ b/Modules/Common/TVMTRC1155Module.php @@ -0,0 +1,358 @@ +version = 1; + } + + final public function post_post_initialize() + { + + } + + final public function pre_process_block($block_id) + { + // Get logs + + $multi_curl = []; + + + $r1 = requester_single($this->select_node(), + endpoint: "/wallet/getblockbynum?num={$block_id}", // no visible=true, because asset_name can be + timeout: $this->timeout); + + $general_data = $r1['transactions'] ?? []; + + // there can be TRC-10 transfers in internal transactions as well + try + { + $receipt_data = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $receipt_data = []; + } + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_getLogs', + 'params' => + [['blockhash' => $this->block_hash, + 'topics' => ['0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'], + ], + ], + 'id' => 0, + ], + timeout: $this->timeout); // TransferSingle + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_getLogs', + 'params' => + [['blockhash' => $this->block_hash, + 'topics' => ['0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb'], + ], + ], + 'id' => 1, + ], + timeout: $this->timeout); // TransferBatch + + // Process logs + + $events = []; + $currencies_to_process = []; + $sort_key = 0; + $logs_batch = []; + + foreach ($receipt_data as $receipt) + { + if (!isset($receipt['log'])) + continue; + + foreach ($receipt['log'] as $log) { + if (!isset($log['topics'])) // this also happens block 34998371 + continue; + if (count($log['topics']) !== 4) + // eth_getLogs works unpredictably in java-tron - doesn't return filtered answer for old blocks, + // so we need to filter the logs manually + continue; + + if ($log['topics'][0] === '4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb') { + $log['id'] = $receipt['id']; + $logs_batch[] = $log; + continue; + } + + if ($log['topics'][0] != 'c3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62') + continue; + + // TransferSingle + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $this->encode_address_to_base58('0x' . $log['address']), + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][2], 24)), + 'sort_key' => $sort_key++, + 'effect' => '-' . to_int256_from_0xhex('0x' . substr($log['data'], 64, 64)), + 'extra' => to_int256_from_0xhex('0x' . substr($log['data'], 0, 64)), + ]; + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $this->encode_address_to_base58('0x' . $log['address']), + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][3], 24)), + 'sort_key' => $sort_key++, + 'effect' => to_int256_from_0xhex('0x' . substr($log['data'], 64, 64)), + 'extra' => to_int256_from_0xhex('0x' . substr($log['data'], 0, 64)), + ]; + + $currencies_to_process[] = '0x' . $log['address']; + } + } + + foreach ($logs_batch as $log) { + if (count($log['topics']) !== 4) + continue; + + $n = str_split($log['data'], 64); + + if (((count($n)) % 2) !== 0) // Some contract may yield invalid `data`, e.g. two token ids, but just one value + continue; + + $n_count = intdiv(count($n), 2); + + if (!$n_count) // Example: Avalanche C-Chain transaction 0x9facbf18cf0be5525459383dcce0b523dc6b62272318d220688c60d2019ee736 + continue; + + $first_5th = $n_count; + + for ($this_n = 0; $this_n < $n_count; $this_n++) { + $events[] = [ + 'transaction' => $log['id'], + 'currency' => $this->encode_address_to_base58('0x' . $log['address']), + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][2], 24)), + 'sort_key' => $sort_key++, + 'effect' => '-' . to_int256_from_0xhex('0x' . $n[$first_5th + $this_n]), + 'extra' => to_int256_from_0xhex('0x' . $n[$this_n]), + ]; + + $events[] = [ + 'transaction' => $log['id'], + 'currency' => $this->encode_address_to_base58('0x' . $log['address']), + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][3], 24)), + 'sort_key' => $sort_key++, + 'effect' => to_int256_from_0xhex('0x' . $n[$first_5th + $this_n]), + 'extra' => to_int256_from_0xhex('0x' . $n[$this_n]), + ]; + } + + $currencies_to_process[] = '0x' . $log['address']; + } + + // Process currencies + $currencies = []; + + $currencies_to_process = array_values(array_unique($currencies_to_process)); // Removing duplicates + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); // Removes already known currencies + + if ($currencies_to_process) + { + $multi_curl = $lib = []; + $this_id = 0; + + foreach ($currencies_to_process as $currency_id) + { + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $currency_id, + 'data' => '0x06fdde03', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Name + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $currency_id, + 'data' => '0x95d89b41', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Symbol + } + + $curl_results = requester_multi($multi_curl, + limit: envm($this->module, 'REQUESTER_THREADS'), + timeout: $this->timeout); + + foreach ($curl_results as $v) + $currency_data[] = requester_multi_process($v, ignore_errors: true); + + reorder_by_id($currency_data); + + foreach ($currency_data as $bit) + { + $this_j = intdiv((int)$bit['id'], 2); + + if (!isset($bit['result']) && isset($bit['error'])) + { + if (str_starts_with($bit['error']['message'], 'REVERT opcode executed')) + $bit['result'] = '0x'; + elseif ($bit['error']['message'] === 'Smart contract is not exist.') + $bit['result'] = '0x'; + else + throw new RequesterException("Request to the node errored with `{$bit['error']['message']}`: " . print_r($bit['error'], true)); + } + + if ((int)$bit['id'] % 2 === 0) + $lib[($currencies_to_process[$this_j])]['name'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + if ((int)$bit['id'] % 2 === 1) + $lib[($currencies_to_process[$this_j])]['symbol'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + } + + foreach ($lib as $id => $l) + { + // This removes invalid UTF-8 sequences + $l['name'] = mb_convert_encoding($l['name'], 'UTF-8', 'UTF-8'); + $l['symbol'] = mb_convert_encoding($l['symbol'], 'UTF-8', 'UTF-8'); + + $currencies[] = [ + 'id' => $this->encode_address_to_base58($id), + 'name' => $l['name'], + 'symbol' => $l['symbol'], + ]; + } + } + + //////////////// + // Processing // + //////////////// + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } + + // Getting balances from the node + public function api_get_balance(string $address, array $currencies): array + { + if (!$currencies) + return []; + + // assuming that address received in base58 format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // should always be the case + try + { + $address = $this->encode_base58_to_evm_hex($address); + } + catch (Exception) + { + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return ; + } + + $address = "0x" . $address; + + if (!preg_match(StandardPatterns::iHexWith0x40->value, $address)) + { + $return = []; + + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return; + } + + $real_currencies = []; + + // Input currencies should be in format like this: `tvm-trc-1155/TXWLT4N9vDcmNHDnSuKv2odhBtizYuEMKJ` + foreach ($currencies as $c) + $real_currencies[] = explode('/', $c)[1]; + + $encoded_address = $this->encode_abi("address", substr($address, 2)); + + $data = $return = []; + + for ($i = 0, $ids = count($real_currencies); $i < $ids; $i++) + { + $data[] = ['jsonrpc' => '2.0', + 'id' => $i, + 'method' => 'eth_call', + 'params' => [['to' => '0x' . $this->encode_base58_to_evm_hex($real_currencies[$i]), + 'data' => '0x70a08231' . $encoded_address, + ], + 'latest', + ], + ]; + } + + $data_chunks = array_chunk($data, 100); + + foreach ($data_chunks as $datai) + { + $result = requester_single($this->select_node(), params: $datai); + + reorder_by_id($result); + + foreach ($result as $bit) + { + $return[] = to_int256_from_0xhex($bit['result'] ?? null); + } + } + return $return; + } +} diff --git a/Modules/Common/TVMTRC20Module.php b/Modules/Common/TVMTRC20Module.php new file mode 100644 index 00000000..ec4e7b23 --- /dev/null +++ b/Modules/Common/TVMTRC20Module.php @@ -0,0 +1,302 @@ +version = 1; + } + + final public function post_post_initialize() + { + + } + + final public function pre_process_block($block_id) + { + // Get logs + try + { + $receipt_data = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}&visible=true", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $receipt_data = []; + } + + // Process logs + + $events = []; + $currencies_to_process = []; + $sort_key = 0; + + foreach ($receipt_data as $receipt) + { + if (!isset($receipt['log'])) + continue; + + foreach ($receipt['log'] as $log) + { + if (!isset($log['topics'])) // this also happens block 34998371 + continue; + + if (count($log['topics']) !== 3 || $log['topics'][0] != 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') + // eth_getLogs works unpredictably in java-tron - doesn't return filtered answer for old blocks, + // so we need to filter the logs manually + continue; + + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $log['address'], + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][1], 24)), + 'sort_key' => $sort_key++, + 'effect' => '-' . to_int256_from_0xhex('0x' . $log['data']), + ]; + + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $log['address'], + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][2], 24)), + 'sort_key' => $sort_key++, + 'effect' => to_int256_from_0xhex('0x' . $log['data']), + ]; + $currencies_to_process[] = $log['address']; + } + } + + // Process currencies + + $currencies = []; + + $currencies_to_process = array_values(array_unique($currencies_to_process)); // Removing duplicates + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); // Removes already known currencies + + if ($currencies_to_process) + { + $multi_curl = $lib = []; + $this_id = 0; + foreach ($currencies_to_process as $currency_id) + { + $evm_address = '0x' . $this->encode_base58_to_evm_hex(strval($currency_id)); + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $evm_address, + 'data' => '0x06fdde03', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Name + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $evm_address, + 'data' => '0x95d89b41', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Symbol + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $evm_address, + 'data' => '0x313ce567', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Decimals + } + + $curl_results = requester_multi($multi_curl, + limit: envm($this->module, 'REQUESTER_THREADS'), + timeout: $this->timeout); + + foreach ($curl_results as $v) + $currency_data[] = requester_multi_process($v, ignore_errors: true); + + reorder_by_id($currency_data); + + foreach ($currency_data as $bit) + { + $this_j = intdiv((int)$bit['id'], 3); + + if (!isset($bit['result']) && isset($bit['error'])) + { + if (str_starts_with($bit['error']['message'], 'REVERT opcode executed')) + $bit['result'] = '0x'; + elseif ($bit['error']['message'] === 'Smart contract is not exist.') + $bit['result'] = '0x'; + else + throw new RequesterException("Request to the node errored with `{$bit['error']['message']}` for " . print_r($currencies_to_process, true)); + } + + if ((int)$bit['id'] % 3 === 0) + $lib[($currencies_to_process[$this_j])]['name'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + if ((int)$bit['id'] % 3 === 1) + $lib[($currencies_to_process[$this_j])]['symbol'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + + if ((int)$bit['id'] % 3 === 2) + { + try + { + $lib[($currencies_to_process[$this_j])]['decimals'] = to_int64_from_0xhex('0x' . substr(substr($bit['result'], 2), -32)); + } + catch (MathException) + { + $lib[($currencies_to_process[$this_j])]['decimals'] = 0; + } + } + } + + foreach ($lib as $id => $l) + { + if ($l['decimals'] > 32767) + $l['decimals'] = 0; // We use SMALLINT for decimals... + + // This removes invalid UTF-8 sequences + $l['name'] = mb_convert_encoding($l['name'], 'UTF-8', 'UTF-8'); + $l['symbol'] = mb_convert_encoding($l['symbol'], 'UTF-8', 'UTF-8'); + + $currencies[] = [ + 'id' => $id, + 'name' => $l['name'], + 'symbol' => $l['symbol'], + 'decimals' => $l['decimals'], + ]; + } + } + + //////////////// + // Processing // + //////////////// + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } + + // Getting balances from the node + function api_get_balance(string $address, array $currencies): array + { + if (!$currencies) + return []; + + + // assuming that address received in base58 format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // should always be the case + try + { + $address = $this->encode_base58_to_evm_hex($address); + } + catch (Exception) + { + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return; + } + $address = "0x" . $address; + + if (!preg_match(StandardPatterns::iHexWith0x40->value, $address)) + { + $return = []; + + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return; + } + + $real_currencies = []; + + // Input currencies should be in format like this: `tron-trc-20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` + foreach ($currencies as $c) + $real_currencies[] = explode('/', $c)[1]; + + $encoded_address = $this->encode_abi("address", substr($address, 2)); + + $data = []; + + for ($i = 0, $ids = count($real_currencies); $i < $ids; $i++) + { + $data[] = ['jsonrpc' => '2.0', + 'id' => $i, + 'method' => 'eth_call', + 'params' => [['to' => '0x' . $this->encode_base58_to_evm_hex($real_currencies[$i]), + 'data' => '0x70a08231' . $encoded_address, + ], + 'latest', + ], + ]; + } + + $return = []; + $data_chunks = array_chunk($data, 100); + + foreach ($data_chunks as $datai) + { + $result = requester_single($this->select_node(), params: $datai); + + reorder_by_id($result); + + foreach ($result as $bit) + { + $return[] = to_int256_from_0xhex($bit['result'] ?? null); + } + } + + return $return; + } +} diff --git a/Modules/Common/TVMTRC721Module.php b/Modules/Common/TVMTRC721Module.php new file mode 100644 index 00000000..62edea1f --- /dev/null +++ b/Modules/Common/TVMTRC721Module.php @@ -0,0 +1,275 @@ +version = 1; + } + + final public function post_post_initialize() + { + // + } + + final public function pre_process_block($block_id) + { + // Get logs + try + { + $receipt_data = requester_single($this->select_node(), + endpoint: "/wallet/gettransactioninfobyblocknum?num={$block_id}&visible=true", + timeout: $this->timeout); + } + catch (RequesterEmptyArrayInResponseException) + { + $receipt_data = []; + } + + // Process logs + + $events = $currencies_to_process = []; + $sort_key = 0; + + foreach ($receipt_data as $receipt) + { + if (!isset($receipt['log'])) + continue; + foreach ($receipt['log'] as $log) + { + if (!isset($log['topics'])) // this also happens block 34998371 + continue; + if (count($log['topics']) !== 4 || $log['topics'][0] != 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') + // eth_getLogs works unpredictably in java-tron - doesn't return filtered answer for old blocks, + // so we need to filter the logs manually + continue; + + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $log['address'], + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][1], 24)), + 'sort_key' => $sort_key++, + 'effect' => '-1', + 'extra' => to_int256_from_0xhex('0x' . $log['topics'][3]) + ]; + + $events[] = [ + 'transaction' => $receipt['id'], + 'currency' => $log['address'], + 'address' => $this->encode_address_to_base58('0x' . substr($log['topics'][2], 24)), + 'sort_key' => $sort_key++, + 'effect' => '1', + 'extra' => to_int256_from_0xhex('0x' . $log['topics'][3]) + ]; + $currencies_to_process[] = $log['address']; + } + + // Process currencies + } + $currencies = []; + + $currencies_to_process = array_values(array_unique($currencies_to_process)); // Removing duplicates + $currencies_to_process = check_existing_currencies($currencies_to_process, $this->currency_format); // Removes already known currencies + + if ($currencies_to_process) + { + $multi_curl = $lib = []; + $this_id = 0; + + foreach ($currencies_to_process as $currency_id) + { + $evm_address = $this->encode_base58_to_evm_hex($currency_id); + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $evm_address, + 'data' => '0x06fdde03', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Name + + $multi_curl[] = requester_multi_prepare($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'params' => [['to' => $evm_address, + 'data' => '0x95d89b41', + ], + 'latest', + ], + 'id' => $this_id++, + ], + timeout: $this->timeout); // Symbol + } + + $curl_results = requester_multi($multi_curl, + limit: envm($this->module, 'REQUESTER_THREADS'), + timeout: $this->timeout); + + foreach ($curl_results as $v) + $currency_data[] = requester_multi_process($v, ignore_errors: true); + + reorder_by_id($currency_data); + + foreach ($currency_data as $bit) + { + $this_j = intdiv((int)$bit['id'], 2); + + if (!isset($bit['result']) && isset($bit['error'])) + { + if (str_starts_with($bit['error']['message'], 'REVERT opcode executed')) + $bit['result'] = '0x'; + elseif ($bit['error']['message'] === 'Smart contract is not exist.') + $bit['result'] = '0x'; + else + throw new RequesterException("Request to the node errored with `{$bit['error']['message']}`"); + } + + if ((int)$bit['id'] % 2 === 0) + $lib[($currencies_to_process[$this_j])]['name'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + if ((int)$bit['id'] % 2 === 1) + $lib[($currencies_to_process[$this_j])]['symbol'] = trim(substr(hex2bin(substr($bit['result'], 2)), -32)); + } + + foreach ($lib as $id => $l) + { + // This removes invalid UTF-8 sequences + $l['name'] = mb_convert_encoding($l['name'], 'UTF-8', 'UTF-8'); + $l['symbol'] = mb_convert_encoding($l['symbol'], 'UTF-8', 'UTF-8'); + + $currencies[] = [ + 'id' => $id, + 'name' => $l['name'], + 'symbol' => $l['symbol'], + ]; + } + } + + //////////////// + // Processing // + //////////////// + + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } + + // Getting balances from the node + function api_get_balance(string $address, array $currencies): array + { + if (!$currencies) + return []; + + // assuming that address received in base58 format THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC + // should always be the case + try + { + $address = $this->encode_base58_to_evm_hex($address); + } + catch (Exception) + { + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return; + } + + $address = "0x" . $address; + + if (!preg_match(StandardPatterns::iHexWith0x40->value, $address)) + { + $return = []; + + foreach ($currencies as $ignored) + $return[] = '0'; + + return $return; + } + + $real_currencies = []; + + // Input currencies should be in format like this: `tvm-trc-20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` + foreach ($currencies as $c) + $real_currencies[] = explode('/', $c)[1]; + + $encoded_address = $this->encode_abi("address", substr($address, 2)); + + $data = []; + + for ($i = 0, $ids = count($real_currencies); $i < $ids; $i++) + { + $data[] = ['jsonrpc' => '2.0', + 'id' => $i, + 'method' => 'eth_call', + 'params' => [['to' => '0x' . $this->encode_base58_to_evm_hex($real_currencies[$i]), + 'data' => '0x70a08231' . $encoded_address, + ], + 'latest', + ], + ]; + } + + $return = []; + $data_chunks = array_chunk($data, 100); + + foreach ($data_chunks as $datai) + { + $result = requester_single($this->select_node(), params: $datai); + + reorder_by_id($result); + + foreach ($result as $bit) + { + $return[] = to_int256_from_0xhex($bit['result'] ?? null); + } + } + + return $return; + } +} diff --git a/Modules/Common/TVMTraits.php b/Modules/Common/TVMTraits.php new file mode 100644 index 00000000..c7a0a636 --- /dev/null +++ b/Modules/Common/TVMTraits.php @@ -0,0 +1,300 @@ +name) { + return $status->value; + } + } + throw new ValueError("New contract type $name investigate the logic" . self::class); + } + + public static function to_assoc_array(): array + { + $result = []; + foreach (self::cases() as $status) { + $result[$status->name] = $status->value; + } + return $result; + } +} + +trait TVMTraits +{ + public function inquire_latest_block() + { + return to_int64_from_0xhex(requester_single($this->select_node(), + params: ['jsonrpc' => '2.0', 'method' => 'eth_blockNumber', 'id' => 0], result_in: 'result', timeout: $this->timeout)); + } + + public function ensure_block($block_id, $break_on_first = false) + { + if ($block_id === MEMPOOL) + { + $this->block_hash = null; + return true; + } + + $multi_curl = []; + $params = ['jsonrpc' => '2.0', 'method' => 'eth_getBlockByNumber', 'params' => [to_0xhex_from_int64($block_id), false], 'id' => 0]; + + foreach ($this->nodes as $node) + { + $multi_curl[] = requester_multi_prepare($node, params: $params, timeout: $this->timeout); + if ($break_on_first) break; + } + + try + { + $curl_results = requester_multi($multi_curl, limit: count($this->nodes), timeout: $this->timeout); + } + catch (RequesterException $e) + { + throw new RequesterException("ensure_block(block_id: {$block_id}): no connection, previously: " . $e->getMessage()); + } + + $result0 = requester_multi_process($curl_results[0], result_in: 'result'); + + + $this->block_hash = $result0['hash']; + $this->block_time = date('Y-m-d H:i:s', to_int64_from_0xhex($result0['timestamp'])); + + if (count($curl_results) > 1) + { + foreach ($curl_results as $result) + { + if (requester_multi_process($result, result_in: 'result')['hash'] !== $this->block_hash) + { + throw new ConsensusException("ensure_block(block_id: {$block_id}): no consensus"); + } + } + } + + $this->block_hash = remove_0x_safely($this->block_hash); + } + + /** + * Encodes 0x... address to Base58 + * in other cases returns the result as is + * 0xA614F803B6FD780986A42C78EC9C7F77E6DED13C -> TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t + * @param string|null $address + * @return string|null + */ + public function encode_address_to_base58(string|null $address): string|null + { + if (!is_null($address) && str_starts_with($address, "0x")) + { + return Base58::hex_to_base58_check("41" . substr($address, 2)); + } + return $address; + } + + /** + * Encodes Base58 address to evm compatible hex + * in other cases returns the result as is + * TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t -> A614F803B6FD780986A42C78EC9C7F77E6DED13C + * @param string|null $address + * @return string|null + * + */ + public function encode_base58_to_evm_hex(string|null $address): string|null + { + if (!is_null($address)) + { + return substr(Base58::base58_check_to_hex($address), 2); + } + return $address; + } + + public function get_exchange_by_id(string|int $id): array|null + { + $exchange = requester_single($this->select_node(), + endpoint: "/wallet/getexchangebyid?id=$id", timeout: $this->timeout); + $exchange['has_trx'] = ($exchange['first_token_id'] === '5f') || ($exchange['second_token_id'] === '5f'); + return $exchange; + } + + function encode_abi(string $flag, string|array $data): string + { + switch ($flag) + { + case 'string': + $length = str_repeat('0', (64 - (strlen(dec2hex((string)strlen($data))) % 64))) . dec2hex((string)strlen(($data))); + $string = bin2hex($data) . str_repeat('0', 64 - (strlen(bin2hex($data)) % 64)); + $result = str_repeat('0', 62) . '40' . $length . $string; + break; + case 'string,uint256': + $length = str_repeat('0', (64 - (strlen(dec2hex((string)strlen($data[0]))) % 64))) . dec2hex((string)strlen(($data[0]))); + $string = bin2hex($data[0]) . str_repeat('0', 64 - (strlen(bin2hex($data[0])) % 64)); + $result = str_repeat('0', 62) . '40' . str_repeat('0', (64 - strlen(dec2hex($data[1])) % 64)) . dec2hex($data[1]) . $length . $string; + break; + case 'uint256': + $result = str_repeat('0', (64 - (strlen(dec2hex($data))) % 64)) . dec2hex($data); + break; + case 'address': + $result = str_repeat('0', (64 - strlen($data))) . $data; + break; + case 'address[]': + $result = str_repeat('0', 64 - strlen(dec2hex((string)count($data)))) . dec2hex(count($data)); + foreach ($data as $address) + $result .= (str_repeat('0', (64 - strlen($address))) . $address); + break; + case 'address,address[]': + if (count($data) !== 2) + throw new DeveloperError('Error number of addresses given to encode_abi function'); + $result = str_repeat('0', (64 - strlen($data[0]))) . $data[0]; //encode address to be parsed + $result .= str_repeat('0', 62) . '40'; //encode the position where to start read array + $result .= str_repeat('0', 64 - strlen(dec2hex((string)count($data[1])))) . dec2hex((string)count($data[1])); //encode number of elements in array + foreach ($data[1] as $address) + $result .= str_repeat('0', (64 - strlen($address))) . $address; + break; + default: + throw new DeveloperError('Unknown flag'); + } + + return $result; + } + + function ens_label_to_hash($label) + { + // All ENS names should conform to the IDNA standard UTS #46 including STD3 Rules, see + // http://unicode.org/reports/tr46/ + + $label = idn_to_ascii($label, IDNA_USE_STD3_RULES, INTL_IDNA_VARIANT_UTS46); + + if (str_contains($label,'.')) + return null; + + return Keccak9::hash($label, 256); + } + + function ens_name_to_hash($name) + { + $node = '0000000000000000000000000000000000000000000000000000000000000000'; + + if (!is_null($name) && (strlen($name) > 0)) + { + $labels = explode('.', $name); + + foreach (array_reverse($labels) as $label) + { + $label_hash = $this->ens_label_to_hash($label); + + if (is_null($label_hash)) + return null; + + $node = $node . $label_hash; + $node = Keccak9::hash(hex2bin($node), 256); + } + } + else + { + return null; + } + + return $node; + } + + function ens_get_data($hash, $function, $registry_contract) + { + $output = requester_single($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'id' => 0, + 'params' => [['to' => $registry_contract, + 'data' => $function . $this->encode_abi('address', $hash), + ], + 'latest', + ], + ], + result_in: 'result', + timeout: $this->timeout); + + return '0x' . substr($output, -40); + } + + function ens_get_data_from_resolver($resolver, $hash, $function, $length = 0) + { + if ($resolver === '0x0000000000000000000000000000000000000000') + return null; + + $output = requester_single($this->select_node(), + params: ['jsonrpc' => '2.0', + 'method' => 'eth_call', + 'id' => 0, + 'params' => [['to' => $resolver, + 'data' => $function . $this->encode_abi('address', $hash), + ], + 'latest', + ], + ], + result_in: 'result', + timeout: $this->timeout); + + return str_replace('0x', '', substr($output, $length)); + } +} + diff --git a/Modules/TronInternalModule.php b/Modules/TronInternalModule.php new file mode 100644 index 00000000..cf656228 --- /dev/null +++ b/Modules/TronInternalModule.php @@ -0,0 +1,20 @@ +blockchain = 'tron'; + $this->module = 'tron-internal'; + $this->complements = 'tron-main'; + $this->is_main = false; + $this->first_block_date = '2018-06-25'; + } +} diff --git a/Modules/TronMainModule.php b/Modules/TronMainModule.php new file mode 100644 index 00000000..de6be7ff --- /dev/null +++ b/Modules/TronMainModule.php @@ -0,0 +1,71 @@ +blockchain = 'tron'; + $this->module = 'tron-main'; + $this->is_main = true; + $this->first_block_id = 0; + $this->first_block_date = '2018-06-25'; + $this->currency = 'tron'; + $this->currency_details = ['name' => 'TRON', 'symbol' => 'TRX', 'decimals' => 6, 'description' => null]; + + // TVMMainModule + $this->extra_features = [TVMSpecialFeatures::AllowEmptyRecipient]; + $this->extra_data_details = array_flip(TVMSpecialTransactions::to_assoc_array()); + $this->reward_function = function ($block_id) { + $sr_reward = '0'; + $partners_reward = '0'; + if ($block_id >= 0 && $block_id <= 14_228_705) { + $sr_reward = '32000000'; + $partners_reward = '16000000'; + } elseif ($block_id > 14_228_705) { + $sr_reward = '16000000'; + $partners_reward = '160000000'; + } + return [$sr_reward, $partners_reward]; + }; + + // Handles + $this->handles_implemented = true; + $this->handles_regex = '/(.*)\.(trx|tron|tns|usdd)/'; // https://forum.trondao.org/t/tns-domains-trx-name-services/16921 + $this->api_get_handle = function ($handle) + { + if (!preg_match($this->handles_regex, $handle)) + return null; + + require_once __DIR__ . '/../Engine/Crypto/Keccak.php'; + + $hash = $this->ens_name_to_hash($handle); + + if (is_null($hash) || $hash === '') + return null; + + $resolver = $this->ens_get_data($hash, '0xc677966d', '0xa209893e28339d8aa2fd3454dc322151c502947e'); + $address = $this->ens_get_data_from_resolver($resolver, $hash, '0x3b3b57de', -40); + $res = null; + if (strlen($address) == 40) + { + try + { + $res = $this->encode_address_to_base58("0x" . $address); + } + catch (Exception) + { + $res = null; + } + + } + return $res; + }; + } +} diff --git a/Modules/TronTRC10Module.php b/Modules/TronTRC10Module.php new file mode 100644 index 00000000..394968a9 --- /dev/null +++ b/Modules/TronTRC10Module.php @@ -0,0 +1,21 @@ +blockchain = 'tron'; + $this->module = 'tron-trc-10'; + $this->is_main = false; + $this->first_block_date = '2018-06-25'; + $this->first_block_id = 0; + } +} diff --git a/Modules/TronTRC1155Module.php b/Modules/TronTRC1155Module.php new file mode 100644 index 00000000..07d1be26 --- /dev/null +++ b/Modules/TronTRC1155Module.php @@ -0,0 +1,21 @@ +blockchain = 'tron'; + $this->module = 'tron-trc-1155'; + $this->is_main = false; + $this->first_block_date = '2018-06-25'; + $this->first_block_id = 0; + } +} diff --git a/Modules/TronTRC20Module.php b/Modules/TronTRC20Module.php new file mode 100644 index 00000000..e61c15e5 --- /dev/null +++ b/Modules/TronTRC20Module.php @@ -0,0 +1,21 @@ +blockchain = 'tron'; + $this->module = 'tron-trc-20'; + $this->is_main = false; + $this->first_block_date = '2018-06-25'; + $this->first_block_id = 0; + } +} diff --git a/Modules/TronTRC721Module.php b/Modules/TronTRC721Module.php new file mode 100644 index 00000000..22e97deb --- /dev/null +++ b/Modules/TronTRC721Module.php @@ -0,0 +1,21 @@ +blockchain = 'tron'; + $this->module = 'tron-trc-721'; + $this->is_main = false; + $this->first_block_date = '2018-06-25'; + $this->first_block_id = 0; + } +}