diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 34c10b3..c99121d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,6 +11,7 @@ - Solana Token module * [Oleg Makaussov](https://github.com/Lorgansar) - Cardano Tokens modules + - Toncoin modules * [Kirill Kuzminykh](https://github.com/Oskal174) - Rootstock modules - Solana Token module diff --git a/Engine/Enums.php b/Engine/Enums.php index 92cafb7..9175a8c 100644 --- a/Engine/Enums.php +++ b/Engine/Enums.php @@ -119,7 +119,7 @@ enum PrivacyModel case Shielded; // The only allowed values are `-?` and `+?` } -// This is for modules where `extra_indexed` is specified: here we can chose which entity it directs to +// This is for modules where `extra_indexed` is specified: here we can choose which entity it directs to enum SearchableEntity: string { case Block = 'block'; @@ -127,4 +127,5 @@ enum SearchableEntity: string case Address = 'address'; case Handle = 'handle'; case Any = 'any'; + case Other = 'other'; // Something that can't be found directly (not really indexed) } diff --git a/Modules/Common/TONLikeJettonModule.php b/Modules/Common/TONLikeJettonModule.php deleted file mode 100644 index 0e5af53..0000000 --- a/Modules/Common/TONLikeJettonModule.php +++ /dev/null @@ -1,245 +0,0 @@ -version = 1; - } - - final public function post_post_initialize() - { - // - } - - final public function pre_process_block($block_id) - { - if ($block_id === 0) // Block #0 is there, but the node doesn't return data for it - { - $this->block_time = date('Y-m-d H:i:s', 0); - $this->set_return_events([]); - $this->set_return_currencies([]); - return; - } - - $events = []; - $currencies_to_process = []; - $sort_key = 0; - - $rq_blocks = []; - $rq_blocks_data = []; - $block_times = []; - - foreach ($this->shards as $shard => $shard_data) - { - $this_root_hash = strtoupper($shard_data['roothash']); - $this_block_hash = strtoupper($shard_data['filehash']); - - $rq_blocks[] = requester_multi_prepare( - $this->select_node(), - endpoint: "blockLite?workchain={$this->workchain}&shard={$shard}&seqno={$block_id}&roothash={$this_root_hash}&filehash={$this_block_hash}", - timeout: $this->timeout - ); - } - - $rq_blocks_multi = requester_multi( - $rq_blocks, - limit: envm($this->module, 'REQUESTER_THREADS'), - timeout: $this->timeout, - valid_codes: [200] - ); - - foreach ($rq_blocks_multi as $v) - $rq_blocks_data[] = requester_multi_process($v, flags: [RequesterOption::RecheckUTF8]); - - foreach ($rq_blocks_data as $block) - { - $block_times[] = (int)$block['header']['time']; - - foreach ($block['transactions'] as $transaction) - { - $transaction['hash'] = strtolower($transaction['hash']); - - if (isset($transaction['messageIn'])) - { - $messageIn = $transaction['messageIn'][0]; // by default in TON there is only 1 message IN - if (isset($messageIn['transfer'])) - { - if ($transaction['messageIn'][0]['transfer']['transfer_type'] === 'transfer_notification' - || $transaction['messageIn'][0]['transfer']['transfer_type'] === 'internal_transfer') - { - $events[] = [ - 'transaction' => $transaction['hash'], - 'currency' => ($transaction['messageIn'][0]['transfer']['token'] !== '') ? $transaction['messageIn'][0]['transfer']['token'] : 'undefined-asset', - 'address' => ($transaction['messageIn'][0]['transfer']['from'] !== '') ? $transaction['messageIn'][0]['transfer']['from'] : 'the-abyss', - 'sort_key' => $sort_key++, - 'effect' => '-' . $transaction['messageIn'][0]['transfer']['amount'], - 'failed' => $transaction['messageIn'][0]['transfer']['failed'], - ]; - - $events[] = [ - 'transaction' => $transaction['hash'], - 'currency' => ($transaction['messageIn'][0]['transfer']['token'] !== '') ? $transaction['messageIn'][0]['transfer']['token'] : 'undefined-asset', - 'address' => ($transaction['messageIn'][0]['destination'] !== '') ? $transaction['messageIn'][0]['destination'] : 'the-abyss', - 'sort_key' => $sort_key++, - 'effect' => $transaction['messageIn'][0]['transfer']['amount'], - 'failed' => $transaction['messageIn'][0]['transfer']['failed'], - ]; - - if ($transaction['messageIn'][0]['transfer']['token'] !== '') - $currencies_to_process[] = ($transaction['messageIn'][0]['transfer']['token'] !== '') ? $transaction['messageIn'][0]['transfer']['token'] : 'undefined-asset'; - } - } - } - } - } - - // 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 = []; - $currency_data = []; - - foreach ($currencies_to_process as $currency_id) - { - if ($currency_id === 'undefined-asset') // here we suppose that it will be only 1 undef_curr and no more - { - $currencies[] = [ - 'id' => 'undefined-asset', - 'name' => '', - 'symbol' => '', - 'decimals' => 0, - ]; - continue; - } - $multi_curl[] = requester_multi_prepare($this->select_node(), - endpoint: "/account?account={$currency_id}&unpack=true", - timeout: $this->timeout); - } - - $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); - - foreach ($currency_data as $account_data) - { - $metadata = []; - if (isset($account_data["contract_state"]["contract_data"]["jetton_content"]["metadata"])) - { - $metadata = $account_data["contract_state"]["contract_data"]["jetton_content"]["metadata"]; - } - // This removes invalid UTF-8 sequences - $currencies[] = [ - 'id' => $account_data['account'], - 'name' => isset($metadata["name"]) ? mb_convert_encoding($metadata["name"], 'UTF-8', 'UTF-8') : '', - 'symbol' => isset($metadata['symbol']) ? mb_convert_encoding($metadata["symbol"], 'UTF-8', 'UTF-8') : '', - 'decimals' => isset($metadata['decimals']) ? ($metadata["decimals"] > 32767 ? 0 : $metadata['decimals']) : 0, - ]; - } - } - - //////////////// - // Processing // - //////////////// - - $max_block_time = date('Y-m-d H:i:s', max($block_times)); - - foreach ($events as &$event) - { - $event['block'] = $block_id; - $event['time'] = $max_block_time; - } - - $this->block_time = $max_block_time; - - $this->set_return_events($events); - $this->set_return_currencies($currencies); - } - - // Getting balances from the node - final function api_get_balance(string $address, array $currencies): array - { - if (!$currencies) - return []; - - $real_currencies = []; - $jetton_array = "["; - - // Input currencies should be in format like this: `ton-jetton/EQDpQ2E8wCsG6OVq_5B3VmCkdD8gRrj124vh-5rh3aKUfDST` - foreach ($currencies as $c) - { - $currency = explode('/', $c)[1]; - $real_currencies[] = $currency; - $jetton_array .= ($currency . ","); - } - - $jetton_array .= "]"; - - $return = []; - - $account_info = requester_single( - $this->select_node(), - endpoint: "account?account={$address}&jettons={$jetton_array}", - timeout: $this->timeout - )['jettons']; - - $account_currencies_info = array_column($account_info, 'balance', 'token'); - - foreach($real_currencies as $c) - { - if (isset($account_currencies_info[$c])) - $return[] = $account_currencies_info[$c]; - else - $return[] = null; - } - - return $return; - } -} diff --git a/Modules/Common/TONLikeMainModule.php b/Modules/Common/TONLikeMainModule.php index 47b4ac5..d934bd6 100644 --- a/Modules/Common/TONLikeMainModule.php +++ b/Modules/Common/TONLikeMainModule.php @@ -20,8 +20,9 @@ abstract class TONLikeMainModule extends CoreModule public ?array $special_addresses = ['the-void']; public ?PrivacyModel $privacy_model = PrivacyModel::Transparent; - public ?array $events_table_fields = ['block', 'transaction', 'sort_key', 'time', 'address', 'effect', 'extra']; - public ?array $events_table_nullable_fields = ['extra']; + public ?array $events_table_fields = ['block', 'transaction', 'sort_key', 'time', 'address', 'effect', 'extra', 'extra_indexed']; + public ?array $events_table_nullable_fields = ['extra', 'extra_indexed']; + public ?SearchableEntity $extra_indexed_hint_entity = SearchableEntity::Transaction; public ?ExtraDataModel $extra_data_model = ExtraDataModel::Default; @@ -30,11 +31,10 @@ abstract class TONLikeMainModule extends CoreModule public ?bool $allow_empty_return_events = true; public ?bool $mempool_implemented = false; - public ?bool $forking_implemented = true; + public ?bool $forking_implemented = false; // Blockchain-specific - public ?array $shards = []; public ?string $workchain = null; // This should be set in the final module // @@ -46,7 +46,7 @@ final public function pre_initialize() final public function post_post_initialize() { - if (is_null($this->workchain)) throw new DeveloperError("`workchain` is not set"); + if (is_null($this->workchain)) throw new DeveloperError('`workchain` is not set'); } final public function pre_process_block($block_id) @@ -58,135 +58,96 @@ final public function pre_process_block($block_id) return; } - $block_times = []; - $events = []; - $sort_key = 0; + $block = requester_single( + $this->select_node(), + endpoint: 'get_blocks/by_master_height', + params: [ + 'args' => [ + $block_id, + true + ] + ], + timeout: $this->timeout); - $rq_blocks = []; - $rq_blocks_data = []; + $events = []; - foreach ($this->shards as $shard => $shard_data) + foreach ($block['transactions'] as $transaction) { - $this_root_hash = strtoupper($shard_data['roothash']); - $this_block_hash = strtoupper($shard_data['filehash']); - - $rq_blocks[] = requester_multi_prepare( - $this->select_node(), - endpoint: "blockLite?workchain={$this->workchain}&shard={$shard}&seqno={$block_id}&roothash={$this_root_hash}&filehash={$this_block_hash}", - timeout: $this->timeout + if (explode(',', substr($transaction['block'], 1), 2)[0] != $this->workchain) // ignore any other chain beside specified + continue; + + $issued_in = (array_key_exists('issued_in', $transaction)) ? [ + 'hash' => $transaction['issued_in']['hash'], + 'fee' => $transaction['issued_in']['fee'] + ] : false; + + [$sub, $add] = $this->generate_event_pair( + $transaction['hash'], + $transaction['account'], + 'the-void', + ($issued_in) ? bcadd($transaction['fee'], $issued_in['fee']) : $transaction['fee'], + $transaction['lt'], + 0, + 'f', + ($issued_in) ? $issued_in['hash'] : null, ); - } - - $rq_blocks_multi = requester_multi( - $rq_blocks, - limit: envm($this->module, 'REQUESTER_THREADS'), - timeout: $this->timeout, - valid_codes: [200] - ); - - foreach ($rq_blocks_multi as $v) - $rq_blocks_data[] = requester_multi_process($v, flags: [RequesterOption::RecheckUTF8]); - - foreach ($rq_blocks_data as $block) - { - $block_times[] = (int)$block['header']['time']; - - foreach ($block['transactions'] as $transaction) - { - $transaction['hash'] = strtolower($transaction['hash']); - - if (!isset($transaction['messageIn'])) - { - if (isset($transaction['fee'])) - throw new ModuleError("There's fee, but no messageIn"); - - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => $transaction['addr'], - 'sort_key' => $sort_key++, - 'effect' => '-0', - 'extra' => 'n', - ]; - - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => 'the-void', - 'sort_key' => $sort_key++, - 'effect' => '0', - 'extra' => 'n', - ]; - } - else - { - if (count($transaction['messageIn']) > 1) - throw new ModuleError('count(messageIn) > 1'); - - $this_message_in = $transaction['messageIn']['0']; - - // Transaction fee - - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => $this_message_in['source'] ?? $transaction['address'], - 'sort_key' => $sort_key++, - 'effect' => '-' . $transaction['fee'], - 'extra' => 'f', - ]; - - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => 'the-void', - 'sort_key' => $sort_key++, - 'effect' => $transaction['fee'], - 'extra' => 'f', - ]; - - // The transfer itself - - if (isset($this_message_in['value'])) - { - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => $this_message_in['source'], - 'sort_key' => $sort_key++, - 'effect' => '-' . $this_message_in['value'], - 'extra' => null, - ]; - - $events[] = [ - 'transaction' => $transaction['hash'], - 'address' => $this_message_in['destination'], - 'sort_key' => $sort_key++, - 'effect' => $this_message_in['value'], - 'extra' => null, - ]; - } - } - } + array_push($events, $sub, $add); + + $is_from_nowhere = $transaction['imsg_src'] === 'NOWHERE'; + $is_to_nowhere = $transaction['imsg_dst'] === 'NOWHERE'; + + [$sub, $add] = $this->generate_event_pair( + $transaction['hash'], + ($is_from_nowhere) ? 'the-void' : $transaction['imsg_src'], + ($is_to_nowhere) ? 'the-void' : $transaction['imsg_dst'], + $transaction['imsg_grams'], + $transaction['lt'], + 1, + ($is_from_nowhere || $is_to_nowhere) ? 'e' : null, + ($issued_in) ? $issued_in['hash'] : null + ); + array_push($events, $sub, $add); } - //////////////// - // Processing // - //////////////// - - $max_block_time = date('Y-m-d H:i:s', max($block_times)); + array_multisort( + array_column($events, 'lt_sort'), SORT_ASC, // first, sort by lt - chronological order is most important + array_column($events, 'transaction'), SORT_ASC, // then, if any tx happened in diff shards, BUT at the same lt - arbitrarily, by tx-hash + array_column($events, 'intra_tx'), SORT_ASC, // lastly, ensure within transaction order: (-fee, +fee, -value, +value) + $events + ); + $sort_key = 0; foreach ($events as &$event) { $event['block'] = $block_id; - $event['time'] = $max_block_time; - } + $event['time'] = $this->block_time; + $event['sort_key'] = $sort_key++; - $this->block_time = $max_block_time; + unset($event['lt_sort']); + unset($event['intra_tx']); + } $this->set_return_events($events); + } // Getting balances from the node final public function api_get_balance(string $address): string { - return (string)requester_single($this->select_node(), - endpoint: "account?account={$address}", - timeout: $this->timeout)['balance']; + $response = requester_single($this->select_node(), + endpoint: 'get_account_info', + params: [ + 'args' => [ + $address, false + ] + ], + timeout: $this->timeout); + if (!array_key_exists('balance', $response)) + return "0"; + + if (!array_key_exists('grams', $response['balance'])) + return "0"; + + return (string)$response['balance']['grams']; } } diff --git a/Modules/Common/TONLikeNFJettonModule.php b/Modules/Common/TONLikeNFJettonModule.php deleted file mode 100644 index 5032fad..0000000 --- a/Modules/Common/TONLikeNFJettonModule.php +++ /dev/null @@ -1,281 +0,0 @@ -version = 1; - } - - final public function post_post_initialize() - { - // - } - - final public function pre_process_block($block_id) - { - if ($block_id === 0) // Block #0 is there, but the node doesn't return data for it - { - $this->block_time = date('Y-m-d H:i:s', 0); - $this->set_return_events([]); - $this->set_return_currencies([]); - return; - } - - $events = []; - $currencies_to_process = []; - $sort_key = 0; - - $rq_blocks = []; - $rq_blocks_data = []; - $block_times = []; - - foreach ($this->shards as $shard => $shard_data) - { - $this_root_hash = strtoupper($shard_data['roothash']); - $this_block_hash = strtoupper($shard_data['filehash']); - - $rq_blocks[] = requester_multi_prepare( - $this->select_node(), - endpoint: "blockLite?workchain={$this->workchain}&shard={$shard}&seqno={$block_id}&roothash={$this_root_hash}&filehash={$this_block_hash}", - timeout: $this->timeout - ); - } - - $rq_blocks_multi = requester_multi( - $rq_blocks, - limit: envm($this->module, 'REQUESTER_THREADS'), - timeout: $this->timeout, - valid_codes: [200] - ); - - foreach ($rq_blocks_multi as $v) - $rq_blocks_data[] = requester_multi_process($v, flags: [RequesterOption::RecheckUTF8]); - - foreach ($rq_blocks_data as $block) - { - $block_times[] = (int)$block['header']['time']; - - foreach ($block['transactions'] as $transaction) - { - $transaction['hash'] = strtolower($transaction['hash']); - - if (isset($transaction['messageIn'])) - { - $messageIn = $transaction['messageIn'][0]; // by default in TON there is only 1 message IN - - if (isset($messageIn['transfer'])) - { - if (in_array($transaction['messageIn'][0]['transfer']['transfer_type'], ["transfer", "deploy_nft"]) - && $transaction['messageIn'][0]['transfer']['type'] === 'nft') - { - $token_info = $this->api_get_nft_info($transaction['messageIn'][0]['transfer']['token']); - - $events[] = [ - 'transaction' => $transaction['hash'], - 'currency' => ($token_info['collection_address'] !== '') ? $token_info['collection_address'] : 'undefined-asset', - 'address' => ($transaction['messageIn'][0]['transfer']['from'] !== '') ? $transaction['messageIn'][0]['transfer']['from'] : 'the-abyss', - 'sort_key' => $sort_key++, - 'effect' => '-1', - 'extra' => $token_info['index'], - 'extra_indexed' => $transaction['messageIn'][0]['transfer']['token'], - 'failed' => $transaction['messageIn'][0]['transfer']['failed'], - ]; - - $events[] = [ - 'transaction' => $transaction['hash'], - 'currency' => ($token_info['collection_address'] !== '') ? $token_info['collection_address'] : 'undefined-asset', - 'address' => ($transaction['messageIn'][0]['transfer']['to'] !== '') ? $transaction['messageIn'][0]['transfer']['to'] : 'the-abyss', - 'sort_key' => $sort_key++, - 'effect' => '1', - 'extra' => $token_info['index'], - 'extra_indexed' => $transaction['messageIn'][0]['transfer']['token'], - 'failed' => $transaction['messageIn'][0]['transfer']['failed'], - ]; - - if ($token_info['collection_address'] !== '') - $currencies_to_process[] = $token_info['collection_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 = []; - $currency_data = []; - - foreach ($currencies_to_process as $currency_id) - { - if ($currency_id === 'undefined-asset') // here we suppose that it will be only 1 undef_curr and no more - { - $currencies[] = [ - 'id' => 'undefined-asset', - 'name' => '', - 'symbol' => '', - ]; - continue; - } - $multi_curl[] = requester_multi_prepare($this->select_node(), - endpoint: "account?account={$currency_id}", - timeout: $this->timeout); - } - - $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); - - foreach ($currency_data as $account_data) - { - $metadata = []; - if (isset($account_data["contract_state"]["contract_data"]["collection_content"]["metadata"])) - { - $metadata = $account_data["contract_state"]["contract_data"]["collection_content"]["metadata"]; - } - $currencies[] = [ - 'id' => $account_data['account'], - 'name' => isset($metadata['name']) ? mb_convert_encoding($metadata['name'], 'UTF-8', 'UTF-8') : '', - 'symbol' => isset($metadata['symbol']) ? mb_convert_encoding($metadata['symbol'], 'UTF-8', 'UTF-8') : '', - ]; - } - } - - //////////////// - // Processing // - //////////////// - - $max_block_time = date('Y-m-d H:i:s', max($block_times)); - - foreach ($events as &$event) - { - $event['block'] = $block_id; - $event['time'] = $max_block_time; - } - - $this->block_time = $max_block_time; - - $this->set_return_events($events); - $this->set_return_currencies($currencies); - } - - // get collection address and index of nft - private function api_get_nft_info($nft) - { - try { - $nft_info = requester_single( - $this->select_node(), - endpoint: "account?account={$nft}", - timeout: $this->timeout, - flags: [RequesterOption::RecheckUTF8] - ); - } catch (RequesterException) - { - $nft_info = null; - } - - if (isset($nft_info['contract_state']['contract_data'])) - { - $contract_data = $nft_info['contract_state']['contract_data']; - return [ - 'collection_address' => isset($contract_data['collection_address']) ? $contract_data['collection_address'] : 'undefined-asset', - 'index' => isset($contract_data['index']) ? $contract_data['index'] : null , - ]; - } - else - return [ - 'collection_address' => 'undefined-asset', - 'index' => null, - ]; - } - - // Getting amount of NFTs from the node by collection - final function api_get_balance(string $address, array $currencies): array - { - if (!$currencies) - return []; - - $real_currencies = []; - $collection_array = "["; - - // Input currencies should be in format like this: `ton-nft/EQDpQ2E8wCsG6OVq_5B3VmCkdD8gRrj124vh-5rh3aKUfDST` - foreach ($currencies as $c) - { - $currency = explode('/', $c)[1]; - $real_currencies[] = $currency; - $collection_array .= ($currency . ","); - } - - $collection_array .= "]"; - - $return = []; - - $account_info = requester_single( - $this->select_node(), - endpoint: "account?account={$address}&nfts_count={$collection_array}", - timeout: $this->timeout - )['nfts_count']; - - $account_currencies_info = array_column($account_info, 'balance', 'token'); - - foreach ($real_currencies as $c) - { - if (isset($account_currencies_info[$c])) - $return[] = $account_currencies_info[$c]; - else - $return[] = null; - } - - return $return; - } -} diff --git a/Modules/Common/TONLikeTokensModule.php b/Modules/Common/TONLikeTokensModule.php new file mode 100644 index 0000000..f1006b1 --- /dev/null +++ b/Modules/Common/TONLikeTokensModule.php @@ -0,0 +1,259 @@ +version = 1; + } + + final public function post_post_initialize() + { + if (is_null($this->workchain)) throw new DeveloperError('`workchain` is not set'); + if (is_null($this->currency_type)) throw new DeveloperError('`currency_type` is not set'); + } + + final public function pre_process_block($block_id) + { + if ($block_id === 0) // Block #0 is there, but the node doesn't return data for it + { + $this->block_time = date('Y-m-d H:i:s', 0); + $this->set_return_events([]); + $this->set_return_currencies([]); + return; + } + + $block = requester_single( + $this->select_node(), + endpoint: 'get_blocks/by_master_height/tokens', + params: [ + 'args' => [ + $block_id, + true + ] + ], + timeout: $this->timeout); + + $events = []; + $currencies_to_process = []; + + foreach ($block['token_transfers'] as $transaction) + { + if (explode(',', substr($transaction['block'], 1), 2)[0] != $this->workchain) // ignore any other chain beside specified + continue; + + if ($transaction['token_type'] == 'Jetton' && $this->currency_type == CurrencyType::NFT) + continue; + + if ($transaction['token_type'] == 'NFT' && $this->currency_type == CurrencyType::FT) + continue; + + [$src, $dst] = $this->remap_participants($transaction); + + // ignore broken transfers and non-std tokens + if ($src == '' || $dst == '' || $transaction['token'] == 'Unknown') + continue; + + [$sub, $add] = $this->generate_event_pair( + $transaction['in_transaction'], + $src, + $dst, + ($transaction['amount'] == "") ? '1' : $transaction['amount'], + $transaction['lt'], + 1, + null, + null + ); + $sub['currency'] = $transaction['token']; + $add['currency'] = $transaction['token']; + + unset($sub['extra_indexed']); + unset($add['extra_indexed']); + + array_push($events, $sub, $add); + + $currencies_to_process[] = $transaction['token']; + } + + array_multisort( + array_column($events, 'lt_sort'), SORT_ASC, // first, sort by lt - chronological order is most important + array_column($events, 'transaction'), SORT_ASC, // then, if any tx happened in diff shards, BUT at the same lt - arbitrarily, by tx-hash + array_column($events, 'intra_tx'), SORT_ASC, // lastly, ensure within transaction order: (-fee, +fee, -value, +value) + $events + ); + + $sort_key = 0; + foreach ($events as &$event) + { + $event['block'] = $block_id; + $event['time'] = $this->block_time; + $event['sort_key'] = $sort_key++; + + unset($event['lt_sort']); + unset($event['intra_tx']); + unset($event['extra']); + } + + $this->set_return_events($events); + + // 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 + + foreach ($currencies_to_process as $currency_id) + { + $currency_data = requester_single( + $this->select_node(), + endpoint: 'get_token_info', + params: [ + 'args' => [$currency_id] + ], + timeout: $this->timeout); + + $next_currency = [ + 'id' => $currency_id, + 'name' => $currency_data['token_name_pretty'] ?? "", + 'symbol' => $currency_data['symbol'] ?? "", + 'decimals' => $currency_data['decimals'] ?? '0' + ]; + + if (array_key_exists('offchain_metadata', $currency_data)) + { + try { + $offchain_data = requester_single( + $currency_data['offchain_metadata'], + endpoint: '', + timeout: $this->timeout); + + $next_currency['name'] = $offchain_data['name'] ?? $next_currency['name']; + $next_currency['symbol'] = $offchain_data['symbol'] ?? $next_currency['symbol']; + $next_currency['decimals'] = $offchain_data['decimals'] ?? $next_currency['decimals']; + } catch (Exception $e) {} + } + + if ($next_currency['name'] == 'Unknown' || $next_currency['name'] == '') + continue; + + $currencies[] = $next_currency; + } + $this->set_return_currencies($currencies); + + } + + // Getting balances from the node + public function api_get_balance(string $address, array $currencies): array + { + if (!$currencies) + return []; + + $real_currencies_map = []; + $return = []; + $i = 0; + // Input currencies should be in format like this: + // `ton-jetton/Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn` - ton-jetton/ + // `ton-nft/Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn` - ton-nft/ + foreach ($currencies as $c) + { + $return[] = '0'; + $real_currencies_map[explode('/', $c)[1]] = $i; + $i++; + } + + $response = requester_single($this->select_node(), + endpoint: 'get_account_info', + params: [ + 'args' => [ + $address, 'tokens', array_keys($real_currencies_map) + ] + ], + timeout: $this->timeout); + + if (!array_key_exists('wallets', $response)) + return $return; + + foreach ($response['wallets'] as $w) + { + if (!array_key_exists('master_contract', $w)) + continue; + + if (!array_key_exists('balance', $w)) + continue; + + if (!array_key_exists($w['master_contract'], $real_currencies_map)) + continue; + + $id = $real_currencies_map[$w['master_contract']]; + + if ($this->currency_type == CurrencyType::FT && array_key_exists('jetton_balance', $w['balance'])) + $return[$id] = bcadd($return[$id], $w['balance']['jetton_balance']); + + if ($this->currency_type == CurrencyType::NFT && array_key_exists('NFT_index', $w['balance'])) + $return[$id] = bcadd($return[$id], '1'); + } + + return $return; + } + + final public function remap_participants($transfer) + { + switch ($transfer['type']) + { + case 'InterWallet': + case 'TransferNotify': + case 'OwnershipAssigned': + case 'JustTransfer': + case 'Transfer': + case 'DeployNFT': + $src = ($transfer['from'] != '') ? $transfer['from'] : $transfer['source']; + $dst = ($transfer['to'] != '') ? $transfer['to'] : $transfer['destination']; + return [$src, $dst]; + + default: + break; + } + return ['', '']; + } +} \ No newline at end of file diff --git a/Modules/Common/TONTraits.php b/Modules/Common/TONTraits.php index ef1acfe..6099ca7 100644 --- a/Modules/Common/TONTraits.php +++ b/Modules/Common/TONTraits.php @@ -1,7 +1,7 @@ select_node(), endpoint: 'lastNum', timeout: $this->timeout); - - return (int)$result[($this->workchain)][(array_key_first($result[($this->workchain)]))]['seqno']; - // This may be not the best solution in case there are several shard with different heights + $result = requester_single( + $this->select_node(), + endpoint: 'get_blocks/by_master_height', + params: [ + 'args' => [ + 'latest', + false + ] + ], + timeout: $this->timeout); + + return (int)$result['seqno']; } public function ensure_block($block_id, $break_on_first = false) { - if ($block_id === 0) // Block #0 is there, but the node doesn't return data for it - { - $this->block_hash = ""; - return; - } - - $multi_curl = []; - - foreach ($this->nodes as $node) - { - $multi_curl[] = requester_multi_prepare($node, endpoint: "getHashByHeight?workchain={$this->workchain}&seqno={$block_id}", 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()); - } - - $hashes = requester_multi_process($curl_results[0]); - ksort($hashes, SORT_STRING); - - $shard_list = $final_filehash = []; - - foreach ($hashes as $shard => $shard_hashes) - { - $shard_list[] = $shard; - $final_filehash[] = $shard_hashes['filehash']; - - $this->shards[$shard] = ['filehash' => $shard_hashes['filehash'], 'roothash' => $shard_hashes['roothash']]; - } - - $this->block_hash = strtolower(implode($final_filehash)); - - $this->block_extra = strtolower(implode('/', $shard_list)); - - if (count($curl_results) > 0) - { - foreach ($curl_results as $result) - { - $this_hashes = requester_multi_process($result); - ksort($this_hashes, SORT_STRING); - - $this_final_filehash = []; - - foreach ($this_hashes as $shard => $shard_hashes) - { - $this_final_filehash[] = $shard_hashes['filehash']; - } + $block = requester_single( + $this->select_node(), + endpoint: 'get_blocks/by_master_height', + params: [ + 'args' => [ + $block_id, + false + ] + ], + timeout: $this->timeout); + + $this->block_hash = strtolower($block['filehash'] . $block['roothash']); + $this->block_time = date('Y-m-d H:i:s', (int)$block['timestamp']); + } - if (strtolower(implode($this_final_filehash)) !== $this->block_hash) - { - throw new ConsensusException("ensure_block(block_id: {$block_id}): no consensus"); - } - } - } + public function generate_event_pair($tx, $src, $dst, $amt, $lt_sort, $intra_tx, $extra, $extra_indexed) + { + $sub = [ + 'transaction' => $tx, + 'address' => $src, + 'effect' => '-' . $amt, + 'lt_sort' => $lt_sort, + 'intra_tx' => 2 * ($intra_tx), + 'extra' => $extra, + 'extra_indexed' => $extra_indexed + ]; + $add = [ + 'transaction' => $tx, + 'address' => $dst, + 'effect' => $amt, + 'lt_sort' => $lt_sort, + 'intra_tx' => 2 * ($intra_tx) + 1, + 'extra' => $extra, + 'extra_indexed' => $extra_indexed + ]; + return [$sub, $add]; } + } diff --git a/Modules/TONJettonModule.php b/Modules/TONJettonModule.php index ec457d2..2468b88 100644 --- a/Modules/TONJettonModule.php +++ b/Modules/TONJettonModule.php @@ -7,7 +7,7 @@ /* This module works with the TEP-74 standard, see * https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md */ -final class TONJettonModule extends TONLikeJettonModule implements Module, MultipleBalanceSpecial +final class TONJettonModule extends TONLikeTokensModule implements Module, MultipleBalanceSpecial { function initialize() { @@ -16,9 +16,15 @@ function initialize() $this->module = 'ton-jetton'; $this->is_main = false; $this->first_block_date = '2019-11-15'; - $this->first_block_id = 0; + $this->first_block_id = 1; - // TONLikeMainModule + // TONLikeTokensModule $this->workchain = '0'; // BaseChain + $this->currency_type = CurrencyType::FT; + + // Tests + $this->tests = [ + ['block' => 40470016, 'result' => 'a:2:{s:6:"events";a:6:{i:0;a:7:{s:11:"transaction";s:64:"27c9ae28bc1cbf57cd78caad716f566beef274d361ea8ab0d4973e68010fbaa4";s:7:"address";s:48:"EQDCTl1v-TPqf-X26w3JVqYokkAYsMz6BQK5Hkd7NeYWYjF8";s:6:"effect";s:13:"-614900000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:0;}i:1;a:7:{s:11:"transaction";s:64:"27c9ae28bc1cbf57cd78caad716f566beef274d361ea8ab0d4973e68010fbaa4";s:7:"address";s:48:"EQAJZF4OyGerASxjdBnsEhJTpLGCC8okIr4ZzkBTTYbZbMhE";s:6:"effect";s:12:"614900000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:1;}i:2;a:7:{s:11:"transaction";s:64:"2eb5cdd0f9d4b600ca3d9d610e6bb9830e7c130431277a93f7f5672b9a24e73b";s:7:"address";s:48:"EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt";s:6:"effect";s:14:"-8569200000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:2;}i:3;a:7:{s:11:"transaction";s:64:"2eb5cdd0f9d4b600ca3d9d610e6bb9830e7c130431277a93f7f5672b9a24e73b";s:7:"address";s:48:"EQDMj00WZdlpEHY1_IQEkfgwanwLs6RhLnu9lxYYj3qlxCQX";s:6:"effect";s:13:"8569200000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:3;}i:4;a:7:{s:11:"transaction";s:64:"815884a27116be0bc704bf64d4669d89e803012bbad1ef4b5be57d3472239697";s:7:"address";s:48:"EQDCTl1v-TPqf-X26w3JVqYokkAYsMz6BQK5Hkd7NeYWYjF8";s:6:"effect";s:14:"-5431500000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:4;}i:5;a:7:{s:11:"transaction";s:64:"815884a27116be0bc704bf64d4669d89e803012bbad1ef4b5be57d3472239697";s:7:"address";s:48:"EQASNZzIszZwju0GPyu7yCYOLuR2_vUYcU11G--PlkVLN69w";s:6:"effect";s:13:"5431500000000";s:8:"currency";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:5:"block";i:40470016;s:4:"time";s:19:"2024-09-18 10:51:10";s:8:"sort_key";i:5;}}s:10:"currencies";a:1:{i:0;a:4:{s:2:"id";s:48:"EQCV5dXNrWVU1z7VidEKvXL5iB_PXs2zm9LLe9bPgSklde0z";s:4:"name";s:6:"NOT-AI";s:6:"symbol";s:5:"NOTAI";s:8:"decimals";s:1:"9";}}}'], + ]; } } diff --git a/Modules/TONMainModule.php b/Modules/TONMainModule.php index c602706..665e082 100644 --- a/Modules/TONMainModule.php +++ b/Modules/TONMainModule.php @@ -15,11 +15,40 @@ function initialize() $this->module = 'ton-main'; $this->is_main = true; $this->currency = 'ton'; - $this->currency_details = ['name' => 'TON', 'symbol' => '💎', 'decimals' => 9, 'description' => null]; + $this->currency_details = ['name' => 'Toncoin', 'symbol' => 'TON', 'decimals' => 9, 'description' => null]; $this->first_block_date = '2019-11-15'; - $this->first_block_id = 0; + $this->first_block_id = 1; // TONLikeMainModule $this->workchain = '0'; // BaseChain + + // Handles + $this->handles_implemented = true; + $this->handles_regex = '/(0|-1):[0-9a-fA-F]{64}|(EQ|UQ)[0-9a-zA-Z_\-]{46}/'; // wc:raw-hex or b64-bounceable (EQ..) or b64-non-bounceable (UQ..) + $this->api_get_handle = function($handle) + { + $response = requester_single($this->select_node(), + endpoint: 'account_serialize', + params: [ + 'args' => [ + $handle + ] + ], + timeout: $this->timeout); + + if (!array_key_exists('valid', $response) || !array_key_exists('base64-bounceable', $response)) + return null; + + if (!$response['valid']) + return null; + + return $response['base64-bounceable']; + }; + + // Tests + $this->tests = [ + // early low activity block with int and ext messages + ['block' => 2000, 'result' => 'a:2:{s:6:"events";a:8:{i:0;a:8:{s:11:"transaction";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:7:"address";s:48:"EQCIUuaay7U5Sum5NPtF0e3iAAAvcR_IkOdtMP8MPNiyscnY";s:6:"effect";s:8:"-5469072";s:5:"extra";s:1:"f";s:13:"extra_indexed";N;s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:0;}i:1;a:8:{s:11:"transaction";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:7:"address";s:8:"the-void";s:6:"effect";s:7:"5469072";s:5:"extra";s:1:"f";s:13:"extra_indexed";N;s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:1;}i:2;a:8:{s:11:"transaction";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:7:"address";s:8:"the-void";s:6:"effect";s:2:"-0";s:5:"extra";s:1:"e";s:13:"extra_indexed";N;s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:2;}i:3;a:8:{s:11:"transaction";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:7:"address";s:48:"EQCIUuaay7U5Sum5NPtF0e3iAAAvcR_IkOdtMP8MPNiyscnY";s:6:"effect";s:1:"0";s:5:"extra";s:1:"e";s:13:"extra_indexed";N;s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:3;}i:4;a:8:{s:11:"transaction";s:64:"c50629714cdad802b5e4f4372c293207b4de5801e8d0bfa7ea173bf0c04ba518";s:7:"address";s:48:"EQC51IjX9oRE0R3mALFJMl_IPw2TEXQDuS3b5N5B9mMv_15k";s:6:"effect";s:8:"-5469072";s:5:"extra";s:1:"f";s:13:"extra_indexed";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:4;}i:5;a:8:{s:11:"transaction";s:64:"c50629714cdad802b5e4f4372c293207b4de5801e8d0bfa7ea173bf0c04ba518";s:7:"address";s:8:"the-void";s:6:"effect";s:7:"5469072";s:5:"extra";s:1:"f";s:13:"extra_indexed";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:5;}i:6;a:8:{s:11:"transaction";s:64:"c50629714cdad802b5e4f4372c293207b4de5801e8d0bfa7ea173bf0c04ba518";s:7:"address";s:48:"EQCIUuaay7U5Sum5NPtF0e3iAAAvcR_IkOdtMP8MPNiyscnY";s:6:"effect";s:17:"-1000000000000000";s:5:"extra";N;s:13:"extra_indexed";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:6;}i:7;a:8:{s:11:"transaction";s:64:"c50629714cdad802b5e4f4372c293207b4de5801e8d0bfa7ea173bf0c04ba518";s:7:"address";s:48:"EQC51IjX9oRE0R3mALFJMl_IPw2TEXQDuS3b5N5B9mMv_15k";s:6:"effect";s:16:"1000000000000000";s:5:"extra";N;s:13:"extra_indexed";s:64:"b907dff4091671be391946fcd05de044283aa17e3fac9d92c87e85c96a5dacf7";s:5:"block";i:2000;s:4:"time";s:19:"2019-11-15 14:32:06";s:8:"sort_key";i:7;}}s:10:"currencies";N;}'], + ]; } } diff --git a/Modules/TONNFJettonModule.php b/Modules/TONNFJettonModule.php deleted file mode 100644 index 2a0733c..0000000 --- a/Modules/TONNFJettonModule.php +++ /dev/null @@ -1,24 +0,0 @@ -blockchain = 'ton'; - $this->module = 'ton-nft'; - $this->is_main = false; - $this->first_block_date = '2019-11-15'; - $this->first_block_id = 0; - - // TONLikeMainModule - $this->workchain = '0'; // BaseChain - } -} diff --git a/Modules/TONNFTModule.php b/Modules/TONNFTModule.php new file mode 100644 index 0000000..2c418f0 --- /dev/null +++ b/Modules/TONNFTModule.php @@ -0,0 +1,30 @@ +blockchain = 'ton'; + $this->module = 'ton-nft'; + $this->is_main = false; + $this->first_block_date = '2019-11-15'; + $this->first_block_id = 1; + + // TONLikeTokensModule + $this->workchain = '0'; // BaseChain + $this->currency_type = CurrencyType::NFT; + + // Tests + $this->tests = [ + ['block' => 40470019, 'result' => 'a:2:{s:6:"events";a:10:{i:0;a:7:{s:11:"transaction";s:64:"27b86bd31997a933857649f84a9d1becc268644d3aaf8e2cb006625e395298e9";s:7:"address";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:6:"effect";s:2:"-1";s:8:"currency";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:0;}i:1;a:7:{s:11:"transaction";s:64:"27b86bd31997a933857649f84a9d1becc268644d3aaf8e2cb006625e395298e9";s:7:"address";s:48:"EQDqaeryhqu_kKxL0SQ8pNNYkrzeqx11zIgdDLPtd79S6jIR";s:6:"effect";s:1:"1";s:8:"currency";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:1;}i:2;a:7:{s:11:"transaction";s:64:"ba80008c916ab46fafd6b5e98f8dd0391a8b43994eb80850e8c08519f589082f";s:7:"address";s:48:"EQCwF7-OQiHBxKePiBFOAWwzHDTFHkJR9likOqTYFQc08OmS";s:6:"effect";s:2:"-1";s:8:"currency";s:48:"EQCVNxC9hPaC6b5nZz-GVy_psUN59Pocj2lXI44jUa0Q6rd8";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:2;}i:3;a:7:{s:11:"transaction";s:64:"ba80008c916ab46fafd6b5e98f8dd0391a8b43994eb80850e8c08519f589082f";s:7:"address";s:48:"EQDX5lXoYcWNwVd1TU4sQ-VsBSOB_NZeg-St9yrRWHQA4rWk";s:6:"effect";s:1:"1";s:8:"currency";s:48:"EQCVNxC9hPaC6b5nZz-GVy_psUN59Pocj2lXI44jUa0Q6rd8";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:3;}i:4;a:7:{s:11:"transaction";s:64:"c6529e2a71c76fa1ae1a5e0af0f0cd06d2f612deebc84987b6dc55227496d677";s:7:"address";s:48:"EQBrnSNI6soNcKlWjXl52APLwWnSrPdw0KIS9B99_xqo2lUf";s:6:"effect";s:2:"-1";s:8:"currency";s:48:"EQD6vxgaarq7nP6tW1sJHMQSzOTWV_PsPcOvck6coW_n3OnX";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:4;}i:5;a:7:{s:11:"transaction";s:64:"c6529e2a71c76fa1ae1a5e0af0f0cd06d2f612deebc84987b6dc55227496d677";s:7:"address";s:48:"EQDG3Pq7UHHzVEOTlj-lEHvOmdK8fDVKD6SyznWL8oqBpNvi";s:6:"effect";s:1:"1";s:8:"currency";s:48:"EQD6vxgaarq7nP6tW1sJHMQSzOTWV_PsPcOvck6coW_n3OnX";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:5;}i:6;a:7:{s:11:"transaction";s:64:"cfd87ca51cc5661dd66ecbfc1a9248230f0da4ab993feeaeb2e1acaf6ae8e4f1";s:7:"address";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:6:"effect";s:2:"-1";s:8:"currency";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:6;}i:7;a:7:{s:11:"transaction";s:64:"cfd87ca51cc5661dd66ecbfc1a9248230f0da4ab993feeaeb2e1acaf6ae8e4f1";s:7:"address";s:48:"EQCaafehYAv8Fy7po2dQoVt9JfepP4A5s9AY0_YKhbTt1fnL";s:6:"effect";s:1:"1";s:8:"currency";s:48:"EQDMp5nZQsqZOKWk8Q3SXmDEOmO7-_D6DP_jHHeSRJe32Y2j";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:7;}i:8;a:7:{s:11:"transaction";s:64:"702f252dee825cb03e82db8581a80341c17b596a4b284e19d60b35b8f1804911";s:7:"address";s:48:"EQAiZeV2VfsPksPAlcGGwOHUGAKnFs27w3GDpV9T45PMIFkH";s:6:"effect";s:2:"-1";s:8:"currency";s:48:"EQAiZeV2VfsPksPAlcGGwOHUGAKnFs27w3GDpV9T45PMIFkH";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:8;}i:9;a:7:{s:11:"transaction";s:64:"702f252dee825cb03e82db8581a80341c17b596a4b284e19d60b35b8f1804911";s:7:"address";s:48:"EQApQ5NY2iROFhV8hwB3u4se4c6oAmW20G60XP4rw5hiiEJl";s:6:"effect";s:1:"1";s:8:"currency";s:48:"EQAiZeV2VfsPksPAlcGGwOHUGAKnFs27w3GDpV9T45PMIFkH";s:5:"block";i:40470019;s:4:"time";s:19:"2024-09-18 10:51:22";s:8:"sort_key";i:9;}}s:10:"currencies";a:0:{}}'], + ]; + } +}