diff --git a/.env.example b/.env.example index 9e419f0c..47329144 100644 --- a/.env.example +++ b/.env.example @@ -656,6 +656,28 @@ MODULE_polygon-zkevm-erc-1155_NODES[]=http://login:password@127.0.0.2:1234/ MODULE_polygon-zkevm-erc-1155_REQUESTER_TIMEOUT=60 MODULE_polygon-zkevm-erc-1155_REQUESTER_THREADS=12 +####################### +## Main Starknet Module +####################### + +MODULES[]=starknet-main +MODULE_starknet-main_CLASS=StarkNetMainModule +MODULE_starknet-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_starknet-main_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_starknet-main_REQUESTER_TIMEOUT=60 +MODULE_starknet-main_REQUESTER_THREADS=12 + +######################## +## Starknet Token Module +######################## + +MODULES[]=starknet-token +MODULE_starknet-token_CLASS=StarkNetTokenModule +MODULE_starknet-token_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_starknet-token_NODES[]=http://login:password@127.0.0.1:1234/ +MODULE_starknet-token_REQUESTER_TIMEOUT=60 +MODULE_starknet-token_REQUESTER_THREADS=12 + ##################### ## Ton Minimal Module ##################### diff --git a/Modules/Common/StarkNetLikeMainModule.php b/Modules/Common/StarkNetLikeMainModule.php new file mode 100644 index 00000000..2d832d47 --- /dev/null +++ b/Modules/Common/StarkNetLikeMainModule.php @@ -0,0 +1,213 @@ +version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->currency)) + throw new DeveloperError("`currency` is not set (developer error)"); + } + + final public function pre_process_block($block_id) + { + $r1 = requester_single( + $this->select_node(), + params: [ + 'method' => 'starknet_getBlockWithTxs', + 'params' => [['block_number' => $block_id]], + 'id' => 0, + 'jsonrpc' => '2.0', + ], + result_in: 'result', + timeout: $this->timeout + ); + + $general_data = $r1['transactions']; + $multi_curl = []; + $ij = 0; + + foreach ($r1['transactions'] as $transaction) { + $multi_curl[] = requester_multi_prepare( + $this->select_node(), + params: [ + 'method' => 'starknet_getTransactionReceipt', + 'params' => [$transaction['transaction_hash']], + 'id' => $ij++, + 'jsonrpc' => '2.0', + ], + timeout: $this->timeout + ); + } + + $curl_results = requester_multi( + $multi_curl, + limit: envm($this->module, 'REQUESTER_THREADS'), + timeout: $this->timeout + ); + + $receipt_data = requester_multi_process_all($curl_results, result_in: 'result'); + + if (($ic = count($general_data)) !== count($receipt_data)) + { + throw new ModuleError('Mismatch in transaction count'); + } + + $sort_key = 0; + + for ($i = 0; $i < $ic; $i++) + { + if ($general_data[$i]['transaction_hash'] !== $receipt_data[$i]['transaction_hash']) + { + throw new ModuleError('Mismatch in transaction order'); + } + + foreach($receipt_data[$i]['events'] as $event) + { + if ( + $event['from_address'] === '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' && + $event['keys'][0] === '0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9' + ) + { + $data = $event['data']; + if(isset($general_data[$i]['sender_address']) && $data[0] === $general_data[$i]['sender_address']) + { + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $general_data[$i]['sender_address'], + 'sort_key' => $sort_key++, + 'effect' => '-' . hex2dec(substr($receipt_data[$i]['actual_fee'], 2)), + 'failed' => ($receipt_data[$i]['execution_status'] === 'SUCCEEDED') ? false : true, + 'extra' => 'f', + ]; + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $data[1], + 'sort_key' => $sort_key++, + 'effect' => hex2dec(substr($receipt_data[$i]['actual_fee'], 2)), + 'failed' => ($receipt_data[$i]['execution_status'] === 'SUCCEEDED') ? false : true, + 'extra' => 'f', + ]; + continue; + } + if($general_data[$i]['type'] === 'DEPLOY_ACCOUNT') + { + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $data[0], + 'sort_key' => $sort_key++, + 'failed' => false, + 'effect' => '-' . to_int256_from_0xhex($data[2]), + 'extra' => 'f', + ]; + + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $data[1], + 'sort_key' => $sort_key++, + 'failed' => false, + 'effect' => to_int256_from_0xhex($data[2]), + 'extra' => 'f', + ]; + continue; + } + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $data[0], + 'sort_key' => $sort_key++, + 'failed' => false, + 'effect' => '-' . to_int256_from_0xhex($data[2]), + 'extra' => null, + ]; + + $events[] = [ + 'transaction' => $general_data[$i]['transaction_hash'], + 'address' => $data[1], + 'sort_key' => $sort_key++, + 'failed' => false, + 'effect' => to_int256_from_0xhex($data[2]), + 'extra' => null, + ]; + } + } + } + + //////////////// + // 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; + } + + $this->set_return_events($events); + } + + // Getting balances from the node + public function api_get_balance($address) + { + $address = strtolower($address); + + if (!preg_match(StandardPatterns::HexWith0x->value, $address)) + return '0'; + + return to_int256_from_0xhex(requester_single( + $this->select_node(), + params: [ + 'method' => 'starknet_call', + 'params' => [ + [ + 'calldata' => ["{$address}"], + 'contract_address' => "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + 'entry_point_selector' => "0x2e4263afad30923c891518314c3c95dbe830a16874e8abc5777a9a20b54c76e" + ], + 'latest', + ], + 'id' => 0, + 'jsonrpc' => '2.0', + ], + result_in: 'result', + timeout: $this->timeout + )[0] ?? '0'); + } +} diff --git a/Modules/Common/StarkNetLikeTokenModule.php b/Modules/Common/StarkNetLikeTokenModule.php new file mode 100644 index 00000000..3c6c7fae --- /dev/null +++ b/Modules/Common/StarkNetLikeTokenModule.php @@ -0,0 +1,347 @@ +version = 1; + } + + final public function post_post_initialize() + { + // + } + + final public function pre_process_block($block_id) + { + $logs = []; + $continuation_token = "{$block_id}-0"; + ASK_REQ: + { + $response = requester_single( + $this->select_node(), + params: [ + 'jsonrpc' => '2.0', + 'method' => 'starknet_getEvents', + 'params' => + [ + "filter" => + [ + 'from_block' => ['block_number' => $this->block_id], + 'to_block' => ['block_number' => $this->block_id], + 'keys' => [['0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9']], + 'chunk_size' => 1000, + 'continuation_token' => $continuation_token, + ], + ], + 'id' => 0, + ], + result_in: 'result', + timeout: $this->timeout + ); + $logs = array_merge($logs, $response['events']); + if (isset($response['continuation_token'])) { + $continuation_token = $response['continuation_token']; + goto ASK_REQ; + } + } + + + $events = []; + $currencies_to_process = []; + $sort_key = 0; + + foreach ($logs as $log) + { + if ($log['block_hash'] !== $this->block_hash) + throw new ModuleError("The node returned wrong data for {$this->block_hash}: {$log['blockHash']}"); + + if (count($log['data']) !== 4) // need to test it + { + continue; // This is ERC-721 + } + + // StarkNet ETH + if($log['from_address'] === '0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7') + { + continue; + } + + $events[] = [ + 'transaction' => $log['transaction_hash'], + 'currency' => $log['from_address'], + 'address' => $log['data'][0], + 'sort_key' => $sort_key++, + 'effect' => '-' . to_int256_from_0xhex($log['data'][2]), + ]; + + $events[] = [ + 'transaction' => $log['transaction_hash'], + 'currency' => $log['from_address'], + 'address' => $log['data'][1], + 'sort_key' => $sort_key++, + 'effect' => to_int256_from_0xhex($log['data'][2]), + ]; + + $currencies_to_process[] = $log['from_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' => 'starknet_call', + 'params' => [ + 'block_id' => 'latest', + 'request' => [ + 'contract_address' => $currency_id, + 'entry_point_selector' => "0x361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60", + 'calldata' => [] + ] + ], + 'id' => $this_id++, + ], + timeout: $this->timeout + ); // Name + + $multi_curl[] = requester_multi_prepare( + $this->select_node(), + params: [ + 'jsonrpc' => '2.0', + 'method' => 'starknet_call', + 'params' => [ + 'block_id' => 'latest', + 'request' => [ + 'contract_address' => $currency_id, + 'entry_point_selector' => "0x216b05c387bab9ac31918a3e61672f4618601f3c598a2f3f2710f37053e1ea4", + 'calldata' => [] + ] + ], + 'id' => $this_id++, + ], + timeout: $this->timeout + ); // Symbol + + $multi_curl[] = requester_multi_prepare( + $this->select_node(), + params: [ + 'jsonrpc' => '2.0', + 'method' => 'starknet_call', + 'params' => [ + 'block_id' => 'latest', + 'request' => [ + 'contract_address' => $currency_id, + 'entry_point_selector' => "0x4c4fb1ab068f6039d5780c68dd0fa2f8742cceb3426d19667778ca7f3518a9", + 'calldata' => [] + ] + ], + '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'][0]) && isset($bit['error']) && (int)$bit['id'] % 3 != 2) + { + if (str_starts_with($bit['error']['message'], 'execution reverted')) + $bit['result'][0] = '0x'; + elseif (str_starts_with($bit['error']['message'], 'invalid opcode')) + $bit['result'][0] = '0x'; + elseif ($bit['error']['message'] === 'out of gas') + $bit['result'][0] = '0x'; + elseif ($bit['error']['message'] === 'invalid jump destination') + $bit['result'][0] = '0x'; + elseif (str_contains($bit['error']['message'], 'Function does not exist')) + $bit['result'][0] = '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'][0], 2)), -32)); + if ((int)$bit['id'] % 3 === 1) + $lib[($currencies_to_process[$this_j])]['symbol'] = trim(substr(hex2bin(substr($bit['result'][0], 2)), -32)); + + if ((int)$bit['id'] % 3 === 2) + { + try + { + if(isset($bit['error'])) + { + $lib[($currencies_to_process[$this_j])]['decimals'] = 0; + } else + { + $lib[($currencies_to_process[$this_j])]['decimals'] = to_int64_from_0xhex('0x' . substr(substr($bit['result'][0], 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 // + //////////////// + + $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; + } + + $this->set_return_events($events); + $this->set_return_currencies($currencies); + } + + // Getting balances from the node + public function api_get_balance($address, array $currencies): array + { + if (!$currencies) + return []; + + if (!preg_match(StandardPatterns::HexWith0x->value, $address)) + { + $return = []; + + foreach ($currencies as $ignoreme) + $return[] = '0'; + + return $return; + } + + $real_currencies = []; + + // Input currencies should be in format like this: `starknet-token/0x058d4802f643d07692ca540dc51a8a33ad1cc364986ad938033e8b89f7b805a0` + foreach ($currencies as $c) + $real_currencies[] = explode('/', $c)[1]; + + $return = []; + $data = []; + + for ($i = 0, $ids = count($real_currencies); $i < $ids; $i++) { + $data[] = requester_multi_prepare( + $this->select_node(), + params: [ + 'jsonrpc' => '2.0', + 'id' => $i, + 'method' => 'starknet_call', + 'params' => [ + [ + 'calldata' => ["{$address}"], + 'contract_address' => $real_currencies[$i], + 'entry_point_selector' => "0x2e4263afad30923c891518314c3c95dbe830a16874e8abc5777a9a20b54c76e" + ], + 'latest', + ], + ], + timeout: $this->timeout + ); + } + + $curl_results = requester_multi( + $data, + 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 $cur_da) + { + $val = isset($cur_da['result'][0]) ? substr($cur_da['result'][0], 0, 66) : null; + $return[] = to_int256_from_0xhex($val); + } + + return $return; + } +} diff --git a/Modules/Common/StarkNetTraits.php b/Modules/Common/StarkNetTraits.php new file mode 100644 index 00000000..2037e79d --- /dev/null +++ b/Modules/Common/StarkNetTraits.php @@ -0,0 +1,62 @@ +select_node(), + params: ['jsonrpc' => '2.0', 'method' => 'starknet_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' => 'starknet_getBlockWithTxHashes', 'params' => [['block_number' => $block_id]], 'id' => 0]; + + $from_nodes = $this->fast_nodes ?? $this->nodes; + + foreach ($from_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($from_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['block_hash']; + $this->block_time = date('Y-m-d H:i:s', (int)$result0['timestamp']); + + if (count($curl_results) > 1) + { + foreach ($curl_results as $result) + { + if (requester_multi_process($result, result_in: 'result')['block_hash'] !== $this->block_hash) + { + throw new ConsensusException("ensure_block(block_id: {$block_id}): no consensus"); + } + } + } + } +} diff --git a/Modules/StarkNetMainModule.php b/Modules/StarkNetMainModule.php new file mode 100644 index 00000000..40420b81 --- /dev/null +++ b/Modules/StarkNetMainModule.php @@ -0,0 +1,23 @@ +blockchain = 'starknet'; + $this->module = 'starknet-main'; + $this->is_main = true; + $this->first_block_date = '2021-11-16'; + $this->first_block_id = 0; + $this->currency = 'Ether'; + $this->currency_details = ['name' => 'Ether', 'symbol' => 'ETH', 'decimals' => 18, 'description' => null]; + $this->mempool_implemented = false; + } +} diff --git a/Modules/StarkNetTokenModule.php b/Modules/StarkNetTokenModule.php new file mode 100644 index 00000000..c19500a3 --- /dev/null +++ b/Modules/StarkNetTokenModule.php @@ -0,0 +1,20 @@ +blockchain = 'starknet'; + $this->module = 'starknet-token'; + $this->is_main = false; + $this->first_block_date = '2021-11-16'; + $this->first_block_id = 0; + } +}