From fa4bcefc4a22cd89f0f05a8c85d84db6ea8b9ec1 Mon Sep 17 00:00:00 2001 From: Kristina Nikolaeva <112946046+kristinaNikolaevaa@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:36:40 +0200 Subject: [PATCH] Ndev 3187 block timestamp number (#414) --- contracts/common/Block.sol | 127 ++++++++++++++ .../test_block_timestamp_block_number.md | 19 ++ .../test_block_timestamp_block_number.py | 165 ++++++++++++++++++ .../tests/neon_evm/test_neon_core_rest_api.py | 24 +++ .../test_transaction_step_from_account.py | 107 +++++++++++- .../tests/neon_evm/utils/neon_api_client.py | 31 ++-- utils/evm_loader.py | 2 +- utils/web3client.py | 12 ++ 8 files changed, 467 insertions(+), 20 deletions(-) create mode 100644 contracts/common/Block.sol create mode 100644 docs/tests/audit/evm/opcodes/test_block_timestamp_block_number.md create mode 100644 integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py diff --git a/contracts/common/Block.sol b/contracts/common/Block.sol new file mode 100644 index 0000000000..30ae86eb9a --- /dev/null +++ b/contracts/common/Block.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + + +contract BlockTimestamp { + event Result(uint256 block_timestamp); + uint256 public a; + uint256 public initial_block_timestamp; + + struct Data { + uint256 value1; + uint256 value2; + } + mapping(uint256 => Data) public dataByTimestamp; + event DataAdded(uint256 timestamp, uint256 value1, uint256 value2); + + constructor() { + initial_block_timestamp = block.timestamp; + } + + function getBlockTimestamp() public view returns (uint256) { + return block.timestamp; + } + + function logTimestamp() public { + emit Result(block.timestamp); + } + + function callIterativeTrx() public payable { + uint256 timestamp_before = block.timestamp; + for (uint256 i = 0; i < 800; i++) { + a = a + block.timestamp; + } + emit Result(block.timestamp); + uint256 timestamp_after = block.timestamp; + + require(timestamp_after == timestamp_before, "Timestamp changed during transaction execution"); + + } + + function addDataToMapping(uint256 _value1, uint256 _value2) public { + uint256 currentTimestamp = block.timestamp % 1000000; + for (uint256 i = 0; i < 5; i++) { + Data memory newData = Data({ + value1: _value1, + value2: _value2 + }); + dataByTimestamp[currentTimestamp] = newData; + emit DataAdded(currentTimestamp, _value1, _value2); + currentTimestamp = currentTimestamp + 1500; + } + } + + function getDataFromMapping(uint256 _timestamp) public view returns (uint256, uint256) { + Data memory retrievedData = dataByTimestamp[_timestamp]; + return (retrievedData.value1, retrievedData.value2); + } + +} + +contract BlockTimestampDeployer { + BlockTimestamp public blockTimestamp; + event Log(address indexed addr); + + constructor() { + blockTimestamp = new BlockTimestamp(); + emit Log(address(blockTimestamp)); + } +} + + +contract BlockNumber { + event Log(address indexed sender, string message); + event Result(uint256 block_number); + + + uint256 public initial_block_number; + + struct Data { + uint256 value1; + uint256 value2; + } + mapping(uint256 => Data) public dataByNumber; + uint256 public a; + + event DataAdded(uint256 number, uint256 value1, uint256 value2); + + constructor() payable { + initial_block_number = block.number; + } + + + function getBlockNumber() public view returns (uint256) { + return block.number; + } + + function logBlockNumber() public { + emit Result(block.number); + } + + function callIterativeTrx() public payable { + uint256 b = 1223; + for (uint256 i = 0; i < 1000; i++) { + a = a + block.number; + } + emit Result(block.number); + } + + function addDataToMapping(uint256 _value1, uint256 _value2) public { + uint256 currentNumber = block.number; + for (uint256 i = 0; i < 5; i++) { + Data memory newData = Data({ + value1: _value1, + value2: _value2 + }); + + dataByNumber[currentNumber] = newData; + emit DataAdded(currentNumber, _value1, _value2); + currentNumber = currentNumber + 1; + } + } + + function getDataFromMapping(uint256 _number) public view returns (uint256, uint256) { + Data memory retrievedData = dataByNumber[_number]; + return (retrievedData.value1, retrievedData.value2); + } +} \ No newline at end of file diff --git a/docs/tests/audit/evm/opcodes/test_block_timestamp_block_number.md b/docs/tests/audit/evm/opcodes/test_block_timestamp_block_number.md new file mode 100644 index 0000000000..14b559b9e0 --- /dev/null +++ b/docs/tests/audit/evm/opcodes/test_block_timestamp_block_number.md @@ -0,0 +1,19 @@ +# Overview + +Check timestamp and block number opcodes. + +# Tests list + +| Test case | Description | XFailed | +|----------------------------------------------------------------------------|------------------------------------------------|---------| +| TestBlockTimestampAndNumber::test_block_timestamp_call | Check contract return timestamp | | +| TestBlockTimestampAndNumber::test_block_timestamp_simple_trx | Check simple transaction with timestamp | | +| TestBlockTimestampAndNumber::test_block_timestamp_iterative | Check iterative transaction with timestamp | | +| TestBlockTimestampAndNumber::test_block_timestamp_constructor | Check timestamp in constructor | | +| TestBlockTimestampAndNumber::test_block_timestamp_in_mapping | Check timestamp as a key in mapping | | +| TestBlockTimestampAndNumber::test_contract_deploys_contract_with_timestamp | Check contract deploys contract with timestamp | | +| TestBlockTimestampAndNumber::test_block_number_call | Check contract return block number | | +| TestBlockTimestampAndNumber::test_block_number_simple_trx | Check simple transaction with block number | | +| TestBlockTimestampAndNumber::test_block_number_iterative | Check iterative transaction with block number | | +| TestBlockTimestampAndNumber::test_block_number_constructor | Check block number in constructor | | +| TestBlockTimestampAndNumber::test_block_number_in_mapping | Check block number as a key in mapping | | diff --git a/integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py b/integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py new file mode 100644 index 0000000000..1428128793 --- /dev/null +++ b/integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py @@ -0,0 +1,165 @@ +import random + +import pytest + +import allure + +from utils.accounts import EthAccounts +from utils.models.result import EthGetBlockByHashResult +from utils.web3client import NeonChainWeb3Client + + +@pytest.fixture(scope="class") +def block_timestamp_contract(web3_client, accounts): + block_timestamp_contract, receipt = web3_client.deploy_and_get_contract( + "common/Block.sol", "0.8.10", accounts[0], contract_name="BlockTimestamp" + ) + return block_timestamp_contract, receipt + + +@pytest.fixture(scope="class") +def block_number_contract(web3_client, accounts): + block_number_contract, receipt = web3_client.deploy_and_get_contract( + "common/Block.sol", "0.8.10", accounts[0], contract_name="BlockNumber" + ) + return block_number_contract, receipt + + +@allure.feature("Opcodes verifications") +@allure.story("Verify block timestamp and block number") +@pytest.mark.usefixtures("accounts", "web3_client") +class TestBlockTimestampAndNumber: + web3_client: NeonChainWeb3Client + accounts: EthAccounts + + def test_block_timestamp_call(self, block_timestamp_contract, json_rpc_client): + contract, _ = block_timestamp_contract + last_block = json_rpc_client.send_rpc("eth_blockNumber", [])["result"] + current_timestamp = json_rpc_client.send_rpc("eth_getBlockByNumber", [last_block, False])["result"][ + "timestamp" + ] + assert hex(contract.functions.getBlockTimestamp().call()) >= current_timestamp + + def test_block_timestamp_simple_trx(self, block_timestamp_contract, json_rpc_client): + contract, _ = block_timestamp_contract + sender_account = self.accounts[0] + + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = contract.functions.logTimestamp().build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_timestamp = EthGetBlockByHashResult(**response).result.timestamp + + event_logs = contract.events.Result().process_receipt(receipt) + assert hex(event_logs[0]["args"]["block_timestamp"]) <= tx_block_timestamp + + def test_block_timestamp_iterative(self, block_timestamp_contract, json_rpc_client): + contract, _ = block_timestamp_contract + sender_account = self.accounts[0] + + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = contract.functions.callIterativeTrx().build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + assert self.web3_client.is_trx_iterative(receipt["transactionHash"].hex()) + + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_timestamp = EthGetBlockByHashResult(**response).result.timestamp + + event_logs = contract.events.Result().process_receipt(receipt) + assert len(event_logs) == 1, "Event logs are not found" + assert hex(event_logs[0]["args"]["block_timestamp"]) <= tx_block_timestamp + + def test_block_timestamp_constructor(self, block_timestamp_contract, json_rpc_client): + contract, receipt = block_timestamp_contract + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_timestamp = EthGetBlockByHashResult(**response).result.timestamp + + assert hex(contract.functions.initial_block_timestamp().call()) <= tx_block_timestamp + + def test_block_timestamp_in_mapping(self, block_timestamp_contract, json_rpc_client): + contract, _ = block_timestamp_contract + sender_account = self.accounts[0] + + v1 = random.randint(1, 100) + v2 = random.randint(1, 100) + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = contract.functions.addDataToMapping(v1, v2).build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert self.web3_client.is_trx_iterative(receipt["transactionHash"].hex()) + assert receipt["status"] == 1 + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_timestamp = EthGetBlockByHashResult(**response).result.timestamp + + event_logs = contract.events.DataAdded().process_receipt(receipt) + added_timestamp = event_logs[0]["args"]["timestamp"] + + assert added_timestamp <= int(tx_block_timestamp, 16) + assert contract.functions.getDataFromMapping(added_timestamp).call() == [v1, v2] + + def test_block_number_call(self, block_number_contract, json_rpc_client): + contract, _ = block_number_contract + current_block_number = json_rpc_client.send_rpc(method="eth_blockNumber", params=[])["result"] + assert hex(contract.functions.getBlockNumber().call()) >= current_block_number + + def test_block_number_simple_trx(self, block_number_contract, json_rpc_client): + contract, _ = block_number_contract + sender_account = self.accounts[0] + + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = contract.functions.logBlockNumber().build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_number = EthGetBlockByHashResult(**response).result.number + + event_logs = contract.events.Result().process_receipt(receipt) + assert hex(event_logs[0]["args"]["block_number"]) <= tx_block_number + + def test_block_number_iterative(self, block_number_contract, json_rpc_client): + contract, _ = block_number_contract + sender_account = self.accounts[0] + + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = contract.functions.callIterativeTrx().build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert self.web3_client.is_trx_iterative(receipt["transactionHash"].hex()) + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_number = EthGetBlockByHashResult(**response).result.number + event_logs = contract.events.Result().process_receipt(receipt) + + assert hex(event_logs[0]["args"]["block_number"]) <= tx_block_number + + def test_block_number_constructor(self, block_number_contract, json_rpc_client): + contract, receipt = block_number_contract + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_number = EthGetBlockByHashResult(**response).result.number + + assert hex(contract.functions.initial_block_number().call()) <= tx_block_number + + def test_contract_deploys_contract_with_timestamp(self, json_rpc_client): + deployer, receipt = self.web3_client.deploy_and_get_contract( + "common/Block.sol", "0.8.10", self.accounts[0], contract_name="BlockTimestampDeployer" + ) + response = json_rpc_client.send_rpc(method="eth_getBlockByHash", params=[receipt["blockHash"].hex(), False]) + tx_block_timestamp = EthGetBlockByHashResult(**response).result.timestamp + + addr = deployer.events.Log().process_receipt(receipt)[0]["args"]["addr"] + contract = self.web3_client.get_deployed_contract(addr, "common/Block.sol", "BlockTimestamp") + assert hex(contract.functions.initial_block_timestamp().call()) <= tx_block_timestamp + + def test_block_number_in_mapping(self, block_number_contract): + contract, _ = block_number_contract + sender_account = self.accounts[0] + + tx = self.web3_client.make_raw_tx(sender_account) + v1 = random.randint(1, 100) + v2 = random.randint(1, 100) + instruction_tx = contract.functions.addDataToMapping(v1, v2).build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert self.web3_client.is_trx_iterative(receipt["transactionHash"].hex()) + assert receipt["status"] == 1 + event_logs = contract.events.DataAdded().process_receipt(receipt) + assert len(event_logs) == 5, "Event logs are not found" + block_number_added = event_logs[0]["args"]["number"] + assert block_number_added <= receipt["blockNumber"] + assert contract.functions.getDataFromMapping(block_number_added).call() == [v1, v2] diff --git a/integration/tests/neon_evm/test_neon_core_rest_api.py b/integration/tests/neon_evm/test_neon_core_rest_api.py index fe37b8c8a4..40fdc01c61 100644 --- a/integration/tests/neon_evm/test_neon_core_rest_api.py +++ b/integration/tests/neon_evm/test_neon_core_rest_api.py @@ -73,6 +73,7 @@ def test_emulate_call_contract_function(neon_api_client, operator_keypair, treas assert result["exit_status"] == "succeed", f"The 'exit_status' field is not succeed. Result: {result}" assert result["steps_executed"] > 0, f"Steps executed amount is 0. Result: {result}" assert result["used_gas"] > 0, f"Used gas is less than 0. Result: {result}" + assert result["is_timestamp_number_used"] is False, f"Value for is_timestamp_number_used is wrong. Result: {result}" assert "Hello World" in to_text(result["result"]) @@ -82,3 +83,26 @@ def test_emulate_with_small_amount_of_steps(neon_api_client, evm_loader, user_ac user_account.eth_address.hex(), contract=None, data=contract_code, max_steps_to_execute=10 ) assert result["exit_status"] == "revert", f"The 'exit_status' field is not revert. Result: {result}" + + +@pytest.mark.parametrize("contract_name", ["BlockTimestamp", "BlockNumber"]) +def test_emulate_call_contract_with_block_timestamp_number(contract_name, neon_api_client, operator_keypair, + treasury_pool, evm_loader): + user_account = evm_loader.make_new_user(operator_keypair) + contract = deploy_contract( + operator_keypair, + user_account, + "common/Block.sol", + evm_loader, + treasury_pool, + version="0.8.10", + contract_name=contract_name, + ) + + result = neon_api_client.emulate_contract_call(user_account.eth_address.hex(), + contract=contract.eth_address.hex(), + function_signature="addDataToMapping(uint256,uint256)", + params=[1, 2]) + + assert result["exit_status"] == "succeed", f"The 'exit_status' field is not succeed. Result: {result}" + assert result["is_timestamp_number_used"], f"Timestamp number is not used. Result: {result}" diff --git a/integration/tests/neon_evm/test_transaction_step_from_account.py b/integration/tests/neon_evm/test_transaction_step_from_account.py index b08d125b88..a7a6af117a 100644 --- a/integration/tests/neon_evm/test_transaction_step_from_account.py +++ b/integration/tests/neon_evm/test_transaction_step_from_account.py @@ -8,17 +8,29 @@ from solana.rpc.core import RPCException as SolanaRPCException from solders.keypair import Keypair from solders.pubkey import Pubkey + from utils.evm_loader import EVM_STEPS from utils.helpers import gen_hash_of_block -from utils.instructions import TransactionWithComputeBudget, make_ExecuteTrxFromAccountDataIterativeOrContinue +from utils.instructions import ( + TransactionWithComputeBudget, + make_ExecuteTrxFromAccountDataIterativeOrContinue, +) from utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT from utils.types import TreasuryPool + from .utils.assert_messages import InstructionAsserts -from .utils.constants import TAG_FINALIZED_STATE, TAG_ACTIVE_STATE -from .utils.contract import make_deployment_transaction, make_contract_call_trx, deploy_contract -from .utils.ethereum import make_eth_transaction, create_contract_address +from .utils.constants import TAG_ACTIVE_STATE, TAG_FINALIZED_STATE +from .utils.contract import ( + deploy_contract, + make_contract_call_trx, + make_deployment_transaction, +) +from .utils.ethereum import create_contract_address, make_eth_transaction from .utils.storage import create_holder -from .utils.transaction_checks import check_transaction_logs_have_text, check_holder_account_tag +from .utils.transaction_checks import ( + check_holder_account_tag, + check_transaction_logs_have_text, +) def generate_access_lists(): @@ -831,6 +843,91 @@ def test_changing_order_of_accounts_for_each_iteration( check_transaction_logs_have_text(resp, "exit_status=0x11") check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + @pytest.mark.parametrize("name", ["BlockTimestamp", "BlockNumber"]) + def test_trx_steps_with_number_timestamp( + self, name, operator_keypair, treasury_pool, neon_api_client, evm_loader, sender_with_tokens + ): + """ + This test repeats the proxy's logic of reemulation with account info overrides and block overrides. + """ + holder = create_holder(operator_keypair, evm_loader) + contract = deploy_contract( + operator_keypair, + sender_with_tokens, + "common/Block.sol", + evm_loader, + treasury_pool, + version="0.8.10", + contract_name=name, + ) + params = [4, 123] + func_signature = "addDataToMapping(uint256,uint256)" + + emulate_result = neon_api_client.emulate_contract_call( + sender_with_tokens.eth_address.hex(), contract.eth_address.hex(), func_signature, params=params + ) + # Accounts to execute the first iteration. + initial_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + signed_tx = make_contract_call_trx(evm_loader, sender_with_tokens, contract, func_signature, params=params) + + operator_balance_pubkey = evm_loader.get_operator_balance_pubkey(operator_keypair) + evm_loader.write_transaction_to_holder_account(signed_tx, holder, operator_keypair) + + def send_transaction_steps_for_holder(accounts): + resp = evm_loader.send_transaction_step_from_account( + operator_keypair, + operator_balance_pubkey, + treasury_pool, + holder, + accounts, + EVM_STEPS, + operator_keypair, + ) + return resp + + def get_account_override(eth_account): + sender_address = eth_account.eth_address.hex() + sender_account_info = neon_api_client.get_balance(sender_address)["value"][0] + + return { + sender_address: {"nonce": sender_account_info["trx_count"], "balance": sender_account_info["balance"]} + } + + def get_block_params(): + block_params = neon_api_client.get_holder(holder)["value"]["block_params"] + block_timestamp, block_number = int(block_params[0], 16), int(block_params[1], 16) + + return {"number": block_number, "time": block_timestamp} + + def make_trace_config(block_params, overrides): + return {"blockOverrides": block_params, "stateOverrides": overrides} + + # State of the sender account should be fetched before the first iteration. + sender_overrides = get_account_override(sender_with_tokens) + send_transaction_steps_for_holder(initial_accounts) + + # Fetch block params after the first iteration as stored in the holder. + block_params = get_block_params() + + # Reemulate after the first iteration + emulate_result = neon_api_client.emulate_contract_call( + sender_with_tokens.eth_address.hex(), + contract.eth_address.hex(), + func_signature, + params=params, + trace_config=make_trace_config(block_params, sender_overrides), + ) + + # Fetch new account list that depends on the re-emulation. + new_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + + # Run the rest of iterations with the new account list. + send_transaction_steps_for_holder(new_accounts) + send_transaction_steps_for_holder(new_accounts) + send_transaction_steps_for_holder(new_accounts) + + check_holder_account_tag(holder, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + class TestStepFromAccountChangingOperatorsDuringTrxRun: def test_next_operator_can_continue_trx( diff --git a/integration/tests/neon_evm/utils/neon_api_client.py b/integration/tests/neon_evm/utils/neon_api_client.py index c0242338af..aef1203d88 100644 --- a/integration/tests/neon_evm/utils/neon_api_client.py +++ b/integration/tests/neon_evm/utils/neon_api_client.py @@ -11,20 +11,25 @@ def __init__(self, url): self.url = url self.headers = {"Content-Type": "application/json"} - def emulate(self, sender, contract, data=bytes(), chain_id=CHAIN_ID, value='0x0', max_steps_to_execute=500000, provide_account_info=None): + def emulate( + self, + sender, + contract, + data=bytes(), + chain_id=CHAIN_ID, + value="0x0", + max_steps_to_execute=500000, + provide_account_info=None, + trace_config=None, + ): if isinstance(data, bytes): data = data.hex() body = { "step_limit": max_steps_to_execute, - "tx": { - "from": sender, - "to": contract, - "data": data, - "chain_id": chain_id, - "value": value - }, + "tx": {"from": sender, "to": contract, "data": data, "chain_id": chain_id, "value": value}, "accounts": [], - "provide_account_info": provide_account_info + "provide_account_info": provide_account_info, + "trace_config": trace_config, } resp = requests.post(url=f"{self.url}/emulate", json=body, headers=self.headers) if resp.status_code == 200: @@ -32,17 +37,14 @@ def emulate(self, sender, contract, data=bytes(), chain_id=CHAIN_ID, value='0x0' else: return resp.json() - - - def emulate_contract_call(self, sender, contract, function_signature, params=None): + def emulate_contract_call(self, sender, contract, function_signature, params=None, value="0x0", trace_config=None): # does not work for tuple in params data = abi.function_signature_to_4byte_selector(function_signature) if params is not None: types = function_signature.split("(")[1].split(")")[0].split(",") data += eth_abi.encode(types, params) - return self.emulate(sender, contract, data) - + return self.emulate(sender, contract, data, value=value, trace_config=trace_config) def get_storage_at(self, contract_id, index="0x0"): body = { @@ -80,3 +82,4 @@ def get_steps_count(self, from_acc, to, data): data ) return result["steps_executed"] + diff --git a/utils/evm_loader.py b/utils/evm_loader.py index 701c605a94..2b363cb681 100644 --- a/utils/evm_loader.py +++ b/utils/evm_loader.py @@ -425,7 +425,7 @@ def execute_transaction_steps_from_account( index += 1 if receipt.value.transaction.meta.err: - raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + raise AssertionError(f"Error in sol trx: {receipt}") for log in receipt.value.transaction.meta.log_messages: if "exit_status" in log: done = True diff --git a/utils/web3client.py b/utils/web3client.py index 3f52b097ff..4d4ce804d6 100644 --- a/utils/web3client.py +++ b/utils/web3client.py @@ -560,6 +560,18 @@ def gas_price_to_eip1559_params(self, gas_price: int) -> tuple[int, int]: max_priority_fee_per_gas = max_fee_per_gas - base_fee_per_gas return max_priority_fee_per_gas, max_fee_per_gas + def is_trx_iterative(self, trx_hash: str) -> bool: + resp = requests.post( + self._proxy_url, + json={ + "jsonrpc": "2.0", + "method": "neon_getSolanaTransactionByNeonTransaction", + "params": [trx_hash], + "id": 0, + }, + ).json() + return len(resp["result"]) > 1 + class NeonChainWeb3Client(Web3Client): def __init__(