diff --git a/conftest.py b/conftest.py index 1cf45af64c..de427d408a 100644 --- a/conftest.py +++ b/conftest.py @@ -11,11 +11,14 @@ from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.runner import runtestprotocol +from solana.rpc.commitment import Confirmed from solders.keypair import Keypair from web3.middleware import geth_poa_middleware from clickfile import TEST_GROUPS, EnvName -from utils.types import TestGroup +from utils.consts import LAMPORT_PER_SOL +from utils.neon_user import NeonUser +from utils.types import TestGroup, TreasuryPool from utils.error_log import error_log from utils import create_allure_environment_opts, setup_logging from utils.faucet import Faucet @@ -254,3 +257,30 @@ def faucet(pytestconfig: Config, web3_client_session) -> Faucet: def accounts_session(pytestconfig: Config, web3_client_session, faucet, eth_bank_account): accounts = EthAccounts(web3_client_session, faucet, eth_bank_account) return accounts + + +@pytest.fixture(scope="function") +def neon_user(evm_loader, pytestconfig) -> NeonUser: + user = NeonUser() + evm_loader.request_airdrop(user.solana_account.pubkey(), 1000 * 10**9, commitment=Confirmed) + evm_loader.deposit_wrapped_sol_from_solana_to_neon( + user.solana_account, "0x" + user.neon_address.hex(), pytestconfig.environment.network_ids["sol"], int(1 * LAMPORT_PER_SOL) + ) + return user + + +@pytest.fixture(scope="session") +def treasury_pool(evm_loader) -> TreasuryPool: + index = 2 + address = evm_loader.create_treasury_pool_address(index) + index_buf = index.to_bytes(4, "little") + evm_loader.request_airdrop(address, 10000 * 10**9, commitment=Confirmed) + return TreasuryPool(index, address, index_buf) + +@pytest.fixture(scope="session") +def treasury_pool_new(evm_loader) -> TreasuryPool: + index = 3 + address = evm_loader.create_treasury_pool_address(index) + index_buf = index.to_bytes(4, "little") + evm_loader.request_airdrop(address, 10000 * 10**9, commitment=Confirmed) + return TreasuryPool(index, address, index_buf) diff --git a/contracts/neon_evm/transfers.sol b/contracts/neon_evm/transfers.sol index 7ff522c458..cc18c3b575 100644 --- a/contracts/neon_evm/transfers.sol +++ b/contracts/neon_evm/transfers.sol @@ -14,9 +14,9 @@ contract transfers { function donateTenPercent() public payable { if (address(this).balance >= 1000) { payable(msg.sender).transfer(address(this).balance / 10); - } - } - + function donate1000() public payable { + payable(msg.sender).transfer(1000); + } } \ No newline at end of file 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 index 1428128793..5c03b352ec 100644 --- a/integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py +++ b/integration/tests/basic/evm/opcodes/test_block_timestamp_block_number.py @@ -9,22 +9,6 @@ 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") diff --git a/integration/tests/basic/rpc/test_rpc_base_calls.py b/integration/tests/basic/rpc/test_rpc_base_calls.py index b4420d2532..fb4e419724 100644 --- a/integration/tests/basic/rpc/test_rpc_base_calls.py +++ b/integration/tests/basic/rpc/test_rpc_base_calls.py @@ -78,15 +78,6 @@ ] -def get_event_signatures(abi: tp.List[tp.Dict]) -> tp.List[str]: - """Get topics as keccak256 from abi Events""" - topics = [] - for event in filter(lambda item: item["type"] == "event", abi): - input_types = ",".join(i["type"] for i in event["inputs"]) - signature = f"{event['name']}({input_types})" - topics.append(f"0x{keccak(signature.encode()).hex()}") - return topics - @allure.feature("JSON-RPC validation") @allure.story("Verify JSON-RPC proxy calls work") diff --git a/integration/tests/basic/solana_signature/test_send_scheduled_transactions.py b/integration/tests/basic/solana_signature/test_send_scheduled_transactions.py new file mode 100644 index 0000000000..e4c6da0c94 --- /dev/null +++ b/integration/tests/basic/solana_signature/test_send_scheduled_transactions.py @@ -0,0 +1,301 @@ +import random + +import allure +import eth_abi +import pytest +from eth_utils import abi +from solana.rpc.core import RPCException +from utils.consts import wSOL +from utils.models.result import EthGetBlockByHashResult +from utils.scheduled_trx import ScheduledTransaction, CreateTreeAccMultipleData + + +@allure.feature("Solana native") +@allure.story("Test sending scheduled transaction") +@pytest.mark.usefixtures("accounts", "web3_client") +class TestScheduledTrx: + def test_send_simple_single_trx( + self, web3_client_sol, neon_user, common_contract, evm_loader, treasury_pool + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + tx = ScheduledTransaction( + neon_user.neon_address, None, nonce, 0, target=common_contract.address, call_data=data + ) + evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), wSOL["address_spl"]) + web3_client_sol.wait_for_transaction_receipt(tx.hash()) + pending_trx = web3_client_sol.get_pending_transactions(neon_user.checksum_address) + assert len(pending_trx) == 1 + assert pending_trx["0x0"][0]["status"] in ("Done", "InProgress") + assert common_contract.functions.getNumber().call() == contract_data + + def test_multiple_scheduled_trx( + self, web3_client_sol, neon_user, common_contract, evm_loader, treasury_pool + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + trxs = [] + max_fee_per_gas = 3000000000 + max_priority_fee_per_gas = 15 + for i in range(4): + trxs.append( + ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=i, + target=common_contract.address, + call_data=data, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + ) + + tree_acc_data = CreateTreeAccMultipleData( + nonce=nonce, max_fee_per_gas=max_fee_per_gas, max_priority_fee_per_gas=max_priority_fee_per_gas + ) + tree_acc_data.add_trx(trxs[0], 3, 0) + tree_acc_data.add_trx(trxs[1], 3, 0) + tree_acc_data.add_trx(trxs[2], 3, 0) + tree_acc_data.add_trx(trxs[3], 0xFFFF, 3) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + + web3_client_sol.send_all_scheduled_transactions(trxs) + for trx in trxs: + resp = web3_client_sol.wait_for_transaction_receipt(trx.hash()) + assert resp["status"] == 1 + pending_trx = web3_client_sol.get_pending_transactions(neon_user.checksum_address) + assert len(pending_trx) == 1 + assert pending_trx["0x0"][0]["status"] == "Done" + assert pending_trx["0x0"][0]["hash"][2:] == trxs[0].hash().hex() + + def test_multiple_scheduled_trx_with_failed_trx( + self, + web3_client_sol, + neon_user, + treasury_pool, + revert_contract_caller, + event_caller_contract, + evm_loader + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + call_data = abi.function_signature_to_4byte_selector("doAssert()") + gas_limit = 30000000 + max_fee_per_gas = 3000000000 + max_priority_fee_per_gas = 15 + + tx0 = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=0, + target=revert_contract_caller.address, + call_data=call_data, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + gas_limit=gas_limit + ) + call_data = abi.function_signature_to_4byte_selector("indexedArgs()") + tx1 = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=1, + target=revert_contract_caller.address, + call_data=call_data, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + gas_limit=gas_limit + ) + tree_acc_data = CreateTreeAccMultipleData( + nonce=nonce, max_fee_per_gas=max_fee_per_gas, max_priority_fee_per_gas=max_priority_fee_per_gas + ) + tree_acc_data.add_trx(tx0, 1, 0) + tree_acc_data.add_trx(tx1, 0xFFFF, 1) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + + web3_client_sol.send_all_scheduled_transactions([tx0, tx1]) + resp1 = web3_client_sol.wait_for_transaction_receipt(tx0.hash()) + assert resp1["status"] == 0 + resp2 = web3_client_sol.wait_for_transaction_receipt(tx1.hash()) + assert resp2["status"] == 0 + pending_trx = web3_client_sol.get_pending_transactions(neon_user.checksum_address) + assert len(pending_trx) == 1 + assert pending_trx["0x0"][0]["status"] == "Done" + assert pending_trx["0x0"][0]["hash"][2:] == tx0.hash().hex() + + def test_create_2_tree_accounts_with_the_same_nonce( + self, web3_client_sol, evm_loader, neon_user, treasury_pool, common_contract + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode(["uint256"], [1]) + tx0 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=0, target=common_contract.address, call_data=data + ) + tree_acc = CreateTreeAccMultipleData(nonce=nonce) + tree_acc.add_trx(tx0, 0xFFFF, 0) + + evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc.data, wSOL["address_spl"], payer_nonce=nonce + ) + with pytest.raises(RPCException, match="transaction with the same nonce already exists"): + evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc.data, wSOL["address_spl"], payer_nonce=nonce + ) + + @pytest.mark.skip("NDEV-3453") + def test_scheduled_trx_send_tokens_to_neon_chain_contract( + self, neon_user, evm_loader, event_caller_contract, web3_client_sol, treasury_pool + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + call_data = abi.function_signature_to_4byte_selector("indexedArgs()") + value = 100000 + tx0 = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=0, + value=value, + target=event_caller_contract.address, + call_data=call_data, + ) + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 0xFFFF, 0) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + web3_client_sol.send_scheduled_transaction(tx0) + receipt = web3_client_sol.wait_for_transaction_receipt(tx0.hash()) + event_logs = event_caller_contract.events.IndexedArgs().process_receipt(receipt) + assert len(event_logs) == 1 + assert len(event_logs[0].args) == 2 + assert event_logs[0].args.who == neon_user.checksum_address + assert event_logs[0].args.value == value + assert event_logs[0].event == "IndexedArgs" + + def test_scheduled_trx_send_tokens_to_sol_chain_contract( + self, neon_user, evm_loader, event_caller_sol_chain, web3_client_sol, treasury_pool + ): + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + call_data = abi.function_signature_to_4byte_selector("indexedArgs()") + value = 100000 + tx0 = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=0, + value=value, + target=event_caller_sol_chain.address, + call_data=call_data, + ) + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 0xFFFF, 0) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + web3_client_sol.send_scheduled_transaction(tx0) + receipt = web3_client_sol.wait_for_transaction_receipt(tx0.hash()) + event_logs = event_caller_sol_chain.events.IndexedArgs().process_receipt(receipt) + assert len(event_logs) == 1 + assert len(event_logs[0].args) == 2 + assert event_logs[0].args.who == neon_user.checksum_address + assert event_logs[0].args.value == value + assert event_logs[0].event == "IndexedArgs" + + def test_scheduled_trx_with_timestamp( + self, block_timestamp_contract, web3_client_sol, neon_user, treasury_pool, evm_loader, json_rpc_client + ): + contract, _ = block_timestamp_contract + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + trx_count = 6 + call_data = [] + for i in range(trx_count): + v1 = random.randint(1, 100) + v2 = random.randint(1, 100) + call_data.append( + abi.function_signature_to_4byte_selector("addDataToMapping(uint256,uint256)") + + eth_abi.encode(["uint256", "uint256"], [v1, v2]) + ) + + gas_limit = 30000000 + max_fee_per_gas = 3000000000 + max_priority_fee_per_gas = 15 + trxs = [] + for i in range(trx_count): + trxs.append( + ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=i, + target=contract.address, + call_data=call_data[i], + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + gas_limit=gas_limit, + ) + ) + + tree_acc_data = CreateTreeAccMultipleData( + nonce=nonce, max_fee_per_gas=max_fee_per_gas, max_priority_fee_per_gas=max_priority_fee_per_gas + ) + tree_acc_data.add_trx(trxs[0], 1, 0) + if trx_count > 2: + for i in range(1, trx_count - 1): + tree_acc_data.add_trx(trxs[i], i + 1, 1) + tree_acc_data.add_trx(trxs[trx_count - 1], 0xFFFF, 1) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + web3_client_sol.send_all_scheduled_transactions(trxs) + receipt = web3_client_sol.wait_for_transaction_receipt(trxs[trx_count - 1].hash()) + 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_scheduled_trx_with_small_gas_limit( + self, + block_timestamp_contract, + web3_client_sol, + neon_user, + treasury_pool, + evm_loader, + event_caller_contract + ): + contract, _ = block_timestamp_contract + + nonce = web3_client_sol.get_nonce(neon_user.checksum_address) + call_data = abi.function_signature_to_4byte_selector("addDataToMapping(uint256,uint256)") + eth_abi.encode( + ["uint256", "uint256"], [1, 2] + ) + gas_limit = 3000000 + max_fee_per_gas = 3000000000 + max_priority_fee_per_gas = 15 + + tx0 = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=0, + target=contract.address, + call_data=call_data, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + gas_limit=gas_limit, + ) + + tree_acc_data = CreateTreeAccMultipleData( + nonce=nonce, max_fee_per_gas=max_fee_per_gas, max_priority_fee_per_gas=max_priority_fee_per_gas + ) + tree_acc_data.add_trx(tx0, 0xFFFF, 0) + evm_loader.create_tree_account_multiple(neon_user, treasury_pool, tree_acc_data.data, wSOL["address_spl"]) + web3_client_sol.send_scheduled_transaction(tx0) + receipt = web3_client_sol.wait_for_transaction_receipt(tx0.hash()) + # for now, it is not possible to see error through the proxy + assert receipt["status"] == 0 diff --git a/integration/tests/conftest.py b/integration/tests/conftest.py index 1bc06232a0..c59028c211 100644 --- a/integration/tests/conftest.py +++ b/integration/tests/conftest.py @@ -238,14 +238,7 @@ def erc20_spl_mintable( @pytest.fixture(scope="class") def class_account_sol_chain( - evm_loader, - solana_account, - web3_client, - web3_client_sol, - faucet, - eth_bank_account, - bank_account, - pytestconfig + evm_loader, solana_account, web3_client, web3_client_sol, faucet, eth_bank_account, bank_account, pytestconfig ) -> LocalAccount: account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) if pytestconfig.environment.use_bank: @@ -261,7 +254,7 @@ def class_account_sol_chain( return account -@pytest.fixture(scope="class") +@pytest.fixture(scope="session") def evm_loader(pytestconfig): return EvmLoader(pytestconfig.environment.evm_loader, pytestconfig.environment.solana_url) @@ -281,7 +274,7 @@ def account_with_all_tokens( operator_keypair, evm_loader_keypair, bank_account, -): +) -> LocalAccount: neon_account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account, amount=500) if web3_client_sol: if pytestconfig.environment.use_bank: @@ -327,8 +320,8 @@ def withdraw_contract(web3_client, faucet, accounts) -> Contract: @pytest.fixture(scope="class") -def common_contract(web3_client, accounts): - contract, _ = web3_client.deploy_and_get_contract( +def common_contract(web3_client, accounts) -> Contract: + contract, tx = web3_client.deploy_and_get_contract( contract="common/Common", version="0.8.12", contract_name="Common", @@ -349,6 +342,12 @@ def event_caller_contract(web3_client, accounts) -> tp.Any: yield event_caller +@pytest.fixture(scope="class") +def event_caller_sol_chain(web3_client_sol, account_with_all_tokens) -> tp.Any: + event_caller, _ = web3_client_sol.deploy_and_get_contract("common/EventCaller", "0.8.12", account_with_all_tokens) + yield event_caller + + @pytest.fixture(scope="class") def event_checker_callee_address(web3_client, accounts) -> tp.Any: _, contract_deploy_tx = web3_client.deploy_and_get_contract( @@ -448,7 +447,6 @@ def sol_price() -> float: return get_sol_price_with_retry() - @pytest.fixture(scope="session") def neon_price(web3_client_session) -> float: """Get NEON price in usd""" @@ -538,13 +536,29 @@ def counter_resource_address(call_solana_caller, accounts, web3_client) -> bytes yield call_solana_caller.functions.getResourceAddress(salt).call() +@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 + + +@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 eip1559_setup( - request: pytest.FixtureRequest, - pytestconfig: Config, - accounts_session: EthAccounts, - web3_client_session: NeonChainWeb3Client, - env_name: EnvName, + request: pytest.FixtureRequest, + pytestconfig: Config, + accounts_session: EthAccounts, + web3_client_session: NeonChainWeb3Client, + env_name: EnvName, ): """ Creates type-2 transactions in the db @@ -560,7 +574,7 @@ def eip1559_setup( block_count = max(block_count, need_eip1559_blocks) # Check if the latest blocks already have enough type-2 transactions. If that's the case - return - fee_history = web3_client_session._web3.eth.fee_history(block_count, 'latest', None) # noqa + fee_history = web3_client_session._web3.eth.fee_history(block_count, "latest", None) # noqa base_fee_per_gas_history = fee_history["baseFeePerGas"] if len(base_fee_per_gas_history) >= block_count + 1: return diff --git a/integration/tests/neon_evm/conftest.py b/integration/tests/neon_evm/conftest.py index 074418b0d3..fe2ce0546c 100644 --- a/integration/tests/neon_evm/conftest.py +++ b/integration/tests/neon_evm/conftest.py @@ -11,7 +11,8 @@ from utils.consts import OPERATOR_KEYPAIR_PATH from utils.evm_loader import EvmLoader from utils.types import Contract, Caller, TreasuryPool -from .utils.constants import NEON_CORE_API_URL, NEON_CORE_API_RPC_URL, SOLANA_URL, EVM_LOADER +from utils.neon_user import NeonUser +from .utils.constants import NEON_CORE_API_URL, NEON_CORE_API_RPC_URL, SOLANA_URL, EVM_LOADER, SOL_CHAIN_ID, CHAIN_ID from .utils.contract import deploy_contract, make_contract_call_trx from .utils.neon_api_rpc_client import NeonApiRpcClient from .utils.storage import create_holder @@ -33,11 +34,11 @@ def prepare_operator(key_file, evm_loader: EvmLoader): evm_loader.request_airdrop(account.pubkey(), 1000 * 10**9, commitment=Confirmed) operator_ether = eth_keys.PrivateKey(account.secret()[:32]).public_key.to_canonical_address() - - ether_balance_pubkey = evm_loader.ether2operator_balance(account, operator_ether) - acc_info = evm_loader.get_account_info(ether_balance_pubkey, commitment=Confirmed) - if acc_info.value is None: - evm_loader.create_operator_balance_account(account, operator_ether) + for chain_id in (SOL_CHAIN_ID, CHAIN_ID): + ether_balance_pubkey = evm_loader.ether2operator_balance(account, operator_ether, chain_id) + acc_info = evm_loader.get_account_info(ether_balance_pubkey, commitment=Confirmed) + if acc_info.value is None: + evm_loader.create_operator_balance_account(account, operator_ether, chain_id) return account @@ -78,14 +79,6 @@ def second_operator_keypair(worker_id, evm_loader) -> Keypair: return prepare_operator(key_file, evm_loader) -@pytest.fixture(scope="session") -def treasury_pool(evm_loader) -> TreasuryPool: - index = 2 - address = evm_loader.create_treasury_pool_address(index) - index_buf = index.to_bytes(4, "little") - return TreasuryPool(index, address, index_buf) - - @pytest.fixture(scope="function") def user_account(evm_loader, operator_keypair) -> Caller: return evm_loader.make_new_user(operator_keypair) @@ -108,6 +101,15 @@ def sender_with_tokens(evm_loader, operator_keypair) -> Caller: return user +@pytest.fixture(scope="session") +def sender_with_wsol(evm_loader, operator_keypair) -> Caller: + user = evm_loader.make_new_user(operator_keypair) + evm_loader.deposit_wrapped_sol_from_solana_to_neon( + user.solana_account, "0x" + user.eth_address.hex(), SOL_CHAIN_ID, 100000 + ) + return user + + @pytest.fixture(scope="session") def holder_acc(operator_keypair: Keypair, evm_loader: EvmLoader) -> Pubkey: return create_holder(operator_keypair, evm_loader) @@ -150,6 +152,24 @@ def string_setter_contract( return deploy_contract(operator_keypair, session_user, "string_setter", evm_loader, treasury_pool) +@pytest.fixture(scope="function") +def basic_contract(evm_loader, operator_keypair, session_user, treasury_pool) -> Contract: + return deploy_contract(operator_keypair, session_user, "common/Common", evm_loader, treasury_pool, version="0.8.12") + + +@pytest.fixture(scope="function") +def spl_token_caller(operator_keypair, evm_loader, session_user, treasury_pool): + return deploy_contract( + operator_keypair, + session_user, + "precompiled/SplTokenCaller", + evm_loader, + treasury_pool, + chain_id=CHAIN_ID, + version="0.8.12", + ) + + @pytest.fixture(scope="session") def calculator_contract( evm_loader: EvmLoader, operator_keypair: Keypair, session_user: Caller, treasury_pool diff --git a/integration/tests/neon_evm/solana_native/__init__.py b/integration/tests/neon_evm/solana_native/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/tests/neon_evm/solana_native/test_multiple_scheduled_transactions.py b/integration/tests/neon_evm/solana_native/test_multiple_scheduled_transactions.py new file mode 100644 index 0000000000..6c92313411 --- /dev/null +++ b/integration/tests/neon_evm/solana_native/test_multiple_scheduled_transactions.py @@ -0,0 +1,244 @@ +import os + +import eth_abi +import pytest +import solana +from eth_utils import abi +from solders.pubkey import Pubkey + +from integration.tests.neon_evm.utils.assert_messages import InstructionAsserts +from integration.tests.neon_evm.utils.contract import get_contract_bin +from integration.tests.neon_evm.utils.ethereum import create_contract_address +from integration.tests.neon_evm.utils.storage import create_holder +from integration.tests.neon_evm.utils.constants import SOL_CHAIN_ID, SOL_MINT_ID +from utils.scheduled_trx import ScheduledTransaction, CreateTreeAccMultipleData +from utils.types import Contract + + +class TestMultipleScheduledTrx: + # ┌───────┐ ┌──────┐ + # │ t0 ✓ ├─>┤ t1 ✓ │ + # │ s=0 │ │ s=1 │ + # └───────┘ └──────┘ + def test_2_depended_transactions( + self, neon_user, basic_contract, evm_loader, treasury_pool, holder_acc, operator_keypair, neon_api_client + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + tx0 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=0, target=basic_contract.eth_address, call_data=data + ) + tx1 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=1, target=basic_contract.eth_address, call_data=data + ) + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 1, 0) + tree_acc_data.add_trx(tx1, 0xFFFF, 1) + print(tree_acc_data.data) + + tree_account = evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc_data.data, SOL_MINT_ID + ) + additional_accounts = [basic_contract.solana_address, neon_user.get_balance_account(SOL_CHAIN_ID)] + print(tree_account) + evm_loader.execute_scheduled_trx_from_instruction( + tx0, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + + holder_acc2 = create_holder(operator_keypair, evm_loader) + evm_loader.execute_scheduled_trx_from_instruction( + tx1, operator_keypair, holder_acc2, tree_account, treasury_pool, additional_accounts + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc2) + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).transactions == [] + + # ┌───────┐ ┌──────┐ + # │ t0 x ├─>┤ t1 ✓ │ + # │ s=0 │ │ s=1 │ + # └───────┘ └──────┘ + def test_2_depended_transactions_one_failed( + self, neon_user, basic_contract, evm_loader, treasury_pool, holder_acc, operator_keypair, neon_api_client + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + tx0 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=0, target=basic_contract.eth_address, value=0 + ) + tx1 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=1, target=basic_contract.eth_address, value=0, call_data=data + ) + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 1, 0) + tree_acc_data.add_trx(tx1, 0xFFFF, 1) + + tree_account = evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc_data.data, SOL_MINT_ID + ) + emulate_result = neon_api_client.emulate( + neon_user.neon_address.hex(), basic_contract.eth_address.hex(), data.hex(), chain_id=SOL_CHAIN_ID + ) + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + evm_loader.execute_scheduled_trx_from_instruction( + tx0, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + + with pytest.raises(solana.rpc.core.RPCException, + match=InstructionAsserts.TRANSACTION_TREE_INVALID_SUCCESS_LIMIT): + evm_loader.execute_scheduled_trx_from_instruction( + tx1, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + + evm_loader.skip_scheduled_trx_from_instruction(tx1, operator_keypair, tree_account, holder_acc) + tree_account_data = neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce) + assert tree_account_data.transactions[0].is_failed() + assert tree_account_data.transactions[1].is_skipped() + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).transactions == [] + + # ┌──────┐ + # │ t0 ✓ │ + # ─┤ s=1 ├─┐ + # └──────┘ │ + # ┌──────┐ │ ┌──────┐ + # ─┤ t1 ✓ ├─┼>┤ t3 ✓ │ + # │ s=1 │ │ │ s=0 │ + # └──────┘ │ └──────┘ + # ┌──────┐ │ + # ─┤ t2 ✓ ├─┘ + # │ s=1 │ + # └──────┘ + def test_tree_with_parallel_trx( + self, evm_loader, neon_user, basic_contract, treasury_pool, holder_acc, operator_keypair, neon_api_client + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + trxs = [] + for i in range(4): + trxs.append( + ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index=i, + target=basic_contract.eth_address, + value=0, + call_data=data, + ) + ) + + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(trxs[0], 3, 0) + tree_acc_data.add_trx(trxs[1], 3, 0) + tree_acc_data.add_trx(trxs[2], 3, 0) + tree_acc_data.add_trx(trxs[3], 0xFFFF, 3) + tree_account = evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc_data.data, SOL_MINT_ID + ) + + additional_accounts = [basic_contract.solana_address, neon_user.get_balance_account(SOL_CHAIN_ID)] + for trx in trxs: + evm_loader.execute_scheduled_trx_from_instruction( + trx, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + tree_account_data = neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce) + assert tree_account_data.all_transactions_successful() + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).transactions == [] + + def test_deploy_and_call_contract( + self, evm_loader, neon_user, neon_api_client, treasury_pool, operator_keypair, holder_acc, basic_contract + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_code = ( + get_contract_bin("common/Common", contract_name="CommonCaller", version="0.8.12") + + eth_abi.encode(["address"], [basic_contract.eth_address.hex()]).hex() + ) + caller_contract: Contract = create_contract_address(neon_user.neon_address, evm_loader, SOL_CHAIN_ID) + + emulate_deploy = neon_api_client.emulate( + neon_user.neon_address.hex(), contract=None, data=contract_code, chain_id=SOL_CHAIN_ID + ) + additional_accounts_deploy = [Pubkey.from_string(item["pubkey"]) for item in emulate_deploy["solana_accounts"]] + + data_call = abi.function_signature_to_4byte_selector("getNumber()") + tx0 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=0, call_data=bytes.fromhex(contract_code), + target=None, gas_limit=193807600 + ) + tx1 = ScheduledTransaction( + neon_user.neon_address, None, nonce, index=1, target=caller_contract.eth_address, call_data=data_call + ) + + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 1, 0) + tree_acc_data.add_trx(tx1, 0xFFFF, 1) + + tree_account = evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc_data.data, SOL_MINT_ID + ) + additional_accounts_call = [ + caller_contract.solana_address, + basic_contract.solana_address, + neon_user.get_balance_account(SOL_CHAIN_ID), + ] + evm_loader.write_transaction_to_holder_account(tx0.encode(), holder_acc, operator_keypair) + evm_loader.execute_scheduled_trx_from_account( + 0, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts_deploy, + compute_unit_price=15 + ) + + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + + evm_loader.execute_scheduled_trx_from_instruction( + tx1, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts_call + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + tree_account_data = neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce) + + assert tree_account_data.all_transactions_successful() + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).transactions == [] + + def test_call_precompiled_by_scheduled_trx( + self, evm_loader, neon_user, neon_api_client, treasury_pool, operator_keypair, holder_acc, spl_token_caller + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + + data = abi.function_signature_to_4byte_selector("initializeMint(uint8)") + eth_abi.encode(["uint8"], [9]) + emulate_result = neon_api_client.emulate( + neon_user.neon_address.hex(), contract=spl_token_caller.eth_address.hex(), data=data, chain_id=SOL_CHAIN_ID + ) + + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + + tx0 = ScheduledTransaction( + neon_user.neon_address, None, nonce, target=spl_token_caller.eth_address, index=0, value=0, call_data=data + ) + + tree_acc_data = CreateTreeAccMultipleData(nonce=nonce) + tree_acc_data.add_trx(tx0, 0xFFFF, 0) + + tree_account = evm_loader.create_tree_account_multiple( + neon_user, treasury_pool, tree_acc_data.data, SOL_MINT_ID + ) + evm_loader.execute_scheduled_trx_from_instruction( + tx0, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).all_transactions_successful() + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).transactions == [] diff --git a/integration/tests/neon_evm/solana_native/test_scheduled_transactions.py b/integration/tests/neon_evm/solana_native/test_scheduled_transactions.py new file mode 100644 index 0000000000..8dfdfdce44 --- /dev/null +++ b/integration/tests/neon_evm/solana_native/test_scheduled_transactions.py @@ -0,0 +1,232 @@ +import os + +import eth_abi +import pytest +import solana +from eth_utils import abi, to_int +from solana.rpc.commitment import Confirmed +from solana.rpc.core import RPCException +from solders.pubkey import Pubkey + +from integration.tests.neon_evm.utils.assert_messages import InstructionAsserts +from integration.tests.neon_evm.utils.contract import deploy_contract +from integration.tests.neon_evm.utils.storage import create_holder +from utils.neon_user import NeonUser +from integration.tests.neon_evm.utils.constants import SOL_CHAIN_ID, SOL_MINT_ID +from utils.scheduled_trx import ScheduledTransaction + + +class TestScheduledTrx: + @pytest.mark.skip("SCHEDULED") + def test_execute_scheduled_trx_from_account( + self, evm_loader, neon_user: NeonUser, treasury_pool, basic_contract, neon_api_client, operator_keypair + ): + holder_acc = create_holder(operator_keypair, evm_loader) + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + 0, + target=basic_contract.eth_address, + value=0, + call_data=data, + ) + tree_account = evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), SOL_MINT_ID) + transaction_tree_data = neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce) + assert transaction_tree_data.get_transaction_count() == 1 + + evm_loader.write_transaction_to_holder_account(tx.encode(), holder_acc, operator_keypair) + additional_accounts = [basic_contract.solana_address, neon_user.get_balance_account(SOL_CHAIN_ID)] + evm_loader.execute_scheduled_trx_from_account( + 0, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts, compute_unit_price=3929 + ) + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).get_transaction_count() == 0 + + @pytest.mark.skip("SCHEDULED") + def test_execute_scheduled_trx_from_instruction( + self, + evm_loader, + neon_user: NeonUser, + treasury_pool, + basic_contract, + neon_api_client, + operator_keypair, + ): + holder_acc = create_holder(operator_keypair, evm_loader) + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + contract_data = 18 + data = abi.function_signature_to_4byte_selector("setNumber(uint256)") + eth_abi.encode( + ["uint256"], [contract_data] + ) + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + 0, + target=basic_contract.eth_address, + value=0, + call_data=data, + ) + + tree_account = evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), SOL_MINT_ID) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).get_transaction_count() == 1 + + additional_accounts = [basic_contract.solana_address, neon_user.get_balance_account(SOL_CHAIN_ID)] + evm_loader.execute_scheduled_trx_from_instruction( + tx, operator_keypair, holder_acc, tree_account, treasury_pool, additional_accounts + ) + + data = abi.function_signature_to_4byte_selector("getNumber()") + result = neon_api_client.emulate( + neon_user.neon_address.hex(), contract=basic_contract.eth_address.hex(), data=data + ) + assert to_int(hexstr=result["result"]) == contract_data + + evm_loader.finish_scheduled_trx(operator_keypair, tree_account, holder_acc) + evm_loader.destroy_tree_account(operator_keypair, neon_user, treasury_pool, tree_account) + assert neon_api_client.get_transaction_tree(neon_user.neon_address.hex(), nonce).get_transaction_count() == 0 + + def test_scheduled_trx_wrong_index( + self, + evm_loader, + neon_user: NeonUser, + treasury_pool, + basic_contract, + neon_api_client, + operator_keypair, + holder_acc, + ): + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + + index = 1 + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + index, + target=basic_contract.eth_address, + value=0, + ) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.TRANSACTION_TREE_INVALID_DATA): + evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), SOL_MINT_ID) + + def test_send_sol_with_zero_fee( + self, evm_loader, neon_user: NeonUser, treasury_pool, neon_api_client, operator_keypair, sender_with_wsol + ): + contract = deploy_contract( + operator_keypair, sender_with_wsol, "transfers", evm_loader, treasury_pool, chain_id=SOL_CHAIN_ID + ) + + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + data = abi.function_signature_to_4byte_selector("donate1000()") + amount = 10000 + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + 0, + target=contract.eth_address, + value=amount, + call_data=data, + gas_limit=25000, + max_fee_per_gas=0, + max_priority_fee_per_gas=0, + ) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.TRANSACTION_TREE_NO_FEE): + evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), SOL_MINT_ID) + + def test_out_of_gas(self, evm_loader, neon_user: NeonUser, treasury_pool, operator_keypair, sender_with_wsol): + contract = deploy_contract( + operator_keypair, sender_with_wsol, "transfers", evm_loader, treasury_pool, chain_id=SOL_CHAIN_ID + ) + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + data = abi.function_signature_to_4byte_selector("donate1000()") + amount = 10000 + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + 0, + target=contract.eth_address, + value=amount, + call_data=data, + gas_limit=20000, + ) + + with pytest.raises(solana.rpc.core.RPCException, match="transaction requires at least 25'000 gas limit"): + evm_loader.create_tree_account(neon_user, treasury_pool, tx.encode(), SOL_MINT_ID) + + @pytest.mark.skip("SCHEDULED") + def test_send_sol_with_priority_fee( + self, + evm_loader, + neon_user: NeonUser, + treasury_pool_new, + neon_api_client, + second_operator_keypair, + sender_with_wsol, + ): + contract = deploy_contract( + second_operator_keypair, sender_with_wsol, "transfers", evm_loader, treasury_pool_new, + chain_id=SOL_CHAIN_ID + ) + holder_acc = create_holder(second_operator_keypair, evm_loader) + nonce = evm_loader.get_neon_nonce(neon_user.neon_address, SOL_CHAIN_ID) + data = abi.function_signature_to_4byte_selector("donate1000()") + amount = 10000 + tx = ScheduledTransaction( + neon_user.neon_address, + None, + nonce, + 0, + target=contract.eth_address, + value=amount, + call_data=data, + ) + operator_balance_before = evm_loader.get_operator_neon_balance(second_operator_keypair, SOL_CHAIN_ID) + user_balance_before = evm_loader.get_neon_balance(neon_user.neon_address, SOL_CHAIN_ID) + treasury_balance_before = evm_loader.get_solana_balance(treasury_pool_new.account) + + tree_account = evm_loader.create_tree_account(neon_user, treasury_pool_new, tx.encode(), SOL_MINT_ID) + + user_balance_after = evm_loader.get_neon_balance(neon_user.neon_address, SOL_CHAIN_ID) + treasury_balance_after = evm_loader.get_solana_balance(treasury_pool_new.account) + tree_account_balance = evm_loader.get_solana_balance(tree_account) + user_balance_diff = user_balance_before - user_balance_after + treasury_balance_diff = treasury_balance_before - treasury_balance_after + + assert treasury_balance_diff > 0 + assert tree_account_balance > 0 + assert user_balance_diff > 0 + + emulate_result = neon_api_client.emulate( + neon_user.neon_address.hex(), contract.eth_address.hex(), data, chain_id=SOL_CHAIN_ID, value=hex(amount) + ) + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + + evm_loader.write_transaction_to_holder_account(tx.encode(), holder_acc, second_operator_keypair) + evm_loader.execute_scheduled_trx_from_account( + 0, + second_operator_keypair, + holder_acc, + tree_account, + treasury_pool_new, + additional_accounts, + compute_unit_price=3929, + ) + evm_loader.finish_scheduled_trx(second_operator_keypair, tree_account, holder_acc) + user_balance_before_destroy = evm_loader.get_neon_balance(neon_user.neon_address, SOL_CHAIN_ID) + + evm_loader.destroy_tree_account(second_operator_keypair, neon_user, treasury_pool_new, tree_account) + user_balance_after_destroy = evm_loader.get_neon_balance(neon_user.neon_address, SOL_CHAIN_ID) + assert user_balance_after_destroy > user_balance_before_destroy + operator_balance_after = evm_loader.get_operator_neon_balance(second_operator_keypair, SOL_CHAIN_ID) + assert operator_balance_after > operator_balance_before diff --git a/integration/tests/neon_evm/test_execute_trx_from_account.py b/integration/tests/neon_evm/test_execute_trx_from_account.py index 19a41d2b21..a3de8c4a1d 100644 --- a/integration/tests/neon_evm/test_execute_trx_from_account.py +++ b/integration/tests/neon_evm/test_execute_trx_from_account.py @@ -7,7 +7,6 @@ from utils.types import Caller - class TestExecuteTrxFromAccount: def test_simple_transfer_transaction( self, operator_keypair, treasury_pool, sender_with_tokens: Caller, session_user: Caller, holder_acc, evm_loader diff --git a/integration/tests/neon_evm/test_interoperability.py b/integration/tests/neon_evm/test_interoperability.py index 3e3b0c22f0..0dacade7b4 100644 --- a/integration/tests/neon_evm/test_interoperability.py +++ b/integration/tests/neon_evm/test_interoperability.py @@ -394,7 +394,7 @@ def test_call_neon_instruction_by_neon_instruction( account_pubkey = evm_loader.ether2balance(caller_ether) contract_pubkey = Pubkey.from_string(evm_loader.ether2program(caller_ether)[0]) - data = bytes([0x30]) + evm_loader.ether2bytes(caller_ether) + CHAIN_ID.to_bytes(8, "little") + data = bytes([0x30]) + caller_ether + CHAIN_ID.to_bytes(8, "little") neon_instruction = Instruction( program_id=evm_loader.loader_id, data=data, 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 6f3b84392e..18f6f1c248 100644 --- a/integration/tests/neon_evm/test_transaction_step_from_account.py +++ b/integration/tests/neon_evm/test_transaction_step_from_account.py @@ -916,8 +916,7 @@ def make_trace_config(block_params, 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"]] - evm_loader.execute_transaction_steps_from_account(operator_keypair,treasury_pool,holder,new_accounts) - + evm_loader.execute_transaction_steps_from_account(operator_keypair,treasury_pool, holder, new_accounts) check_holder_account_tag(holder, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) diff --git a/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py b/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py index 19616dcf0c..b95e8e2c13 100644 --- a/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py +++ b/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py @@ -146,7 +146,7 @@ def test_transaction_with_access_list( ) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - error = re.escape("assertion failed: trx.chain_id().is_none()") + error = re.escape("invalid chainId") with pytest.raises(solana.rpc.core.RPCException, match=error): evm_loader.execute_transaction_steps_from_account_no_chain_id( operator_keypair, diff --git a/integration/tests/neon_evm/utils/assert_messages.py b/integration/tests/neon_evm/utils/assert_messages.py index 1603ba50e0..e2b24fa4b5 100644 --- a/integration/tests/neon_evm/utils/assert_messages.py +++ b/integration/tests/neon_evm/utils/assert_messages.py @@ -1,5 +1,5 @@ class InstructionAsserts: - INVALID_CHAIN_ID = "Invalid Chain ID" + INVALID_CHAIN_ID = "invalid chainId" INVALID_NONCE = "Invalid Nonce" TRX_ALREADY_FINALIZED = "Transaction already finalized" INSUFFICIENT_FUNDS = "Insufficient balance" @@ -15,3 +15,7 @@ class InstructionAsserts: INVALID_OPERATOR_KEY = "operator.key != storage.operator" HOLDER_OVERFLOW = "Checked Integer Math Overflow" HOLDER_INSUFFICIENT_SIZE = "Holder Account - insufficient size" + TRANSACTION_TREE_INVALID_DATA = "Transaction Tree - invalid transaction data" + TRANSACTION_TREE_INVALID_SUCCESS_LIMIT = "Transaction Tree - transaction invalid success execute limit" + TRANSACTION_TREE_NO_FEE = "Transaction Tree - transaction requires at least 1 gwei for gas price" + diff --git a/integration/tests/neon_evm/utils/constants.py b/integration/tests/neon_evm/utils/constants.py index 0bfff01a61..2e70aae705 100644 --- a/integration/tests/neon_evm/utils/constants.py +++ b/integration/tests/neon_evm/utils/constants.py @@ -21,4 +21,7 @@ NEON_TOKEN_MINT_ID: Pubkey = Pubkey.from_string( os.environ.get("NEON_TOKEN_MINT") or "HPsV9Deocecw3GeZv1FkAPNCBRfuVyfw9MMwjwRe1xaU" ) +SOL_MINT_ID: Pubkey = Pubkey.from_string("So11111111111111111111111111111111111111112") + CHAIN_ID = int(os.environ.get("NEON_CHAIN_ID", 111)) +SOL_CHAIN_ID = int(os.environ.get("SOL_CHAIN_ID", 112)) diff --git a/integration/tests/neon_evm/utils/contract.py b/integration/tests/neon_evm/utils/contract.py index 41906fe3a5..33ca7e063e 100644 --- a/integration/tests/neon_evm/utils/contract.py +++ b/integration/tests/neon_evm/utils/contract.py @@ -9,8 +9,9 @@ from solders.pubkey import Pubkey from utils.evm_loader import EvmLoader +from utils.neon_user import NeonUser from utils.types import Caller, TreasuryPool, Contract -from .constants import NEON_CORE_API_URL +from .constants import NEON_CORE_API_URL, CHAIN_ID from .neon_api_client import NeonApiClient from .transaction_checks import check_transaction_logs_have_text @@ -75,11 +76,14 @@ def make_deployment_transaction( data = get_contract_bin(contract_file_name, contract_name, version) if encoded_args is not None: data = data + encoded_args.hex() - - nonce = evm_loader.get_neon_nonce(user.eth_address) + if chain_id: + nonce = evm_loader.get_neon_nonce(user.eth_address, chain_id) + else: + nonce = evm_loader.get_neon_nonce(user.eth_address) tx = {"to": None, "value": 0, "gas": gas, "gasPrice": 0, "nonce": nonce, "data": data} if chain_id: tx["chainId"] = chain_id + if access_list: tx["accessList"] = access_list tx["type"] = 1 @@ -145,6 +149,7 @@ def deploy_contract( evm_loader: EvmLoader, treasury_pool: TreasuryPool, value: int = 0, + chain_id=CHAIN_ID, encoded_args=None, contract_name: tp.Optional[str] = None, version: str = "0.7.6", @@ -155,17 +160,30 @@ def deploy_contract( if encoded_args is None: encoded_args = b"" emulate_result = neon_api_client.emulate( - user.eth_address.hex(), contract=None, data=contract_code + encoded_args.hex() + user.eth_address.hex(), + contract=None, + data=contract_code + encoded_args.hex(), + chain_id=chain_id, + value=hex(value), ) additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] - contract: Contract = create_contract_address(user, evm_loader) + contract: Contract = create_contract_address(user, evm_loader, chain_id) holder_acc = create_holder(operator, evm_loader) signed_tx = make_deployment_transaction( - evm_loader, user, contract_file_name, contract_name, encoded_args=encoded_args, value=value, version=version + evm_loader, + user, + contract_file_name, + contract_name, + encoded_args=encoded_args, + value=value, + version=version, + chain_id=chain_id, ) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator) - resp = evm_loader.execute_transaction_steps_from_account(operator, treasury_pool, holder_acc, additional_accounts) + resp = evm_loader.execute_transaction_steps_from_account( + operator, treasury_pool, holder_acc, additional_accounts, chain_id=chain_id + ) check_transaction_logs_have_text(resp, "exit_status=0x12") return contract diff --git a/integration/tests/neon_evm/utils/ethereum.py b/integration/tests/neon_evm/utils/ethereum.py index 3888645e42..ca338efb84 100644 --- a/integration/tests/neon_evm/utils/ethereum.py +++ b/integration/tests/neon_evm/utils/ethereum.py @@ -9,15 +9,17 @@ from .eth_tx_utils import pack -def create_contract_address(user: Caller, evm_loader: EvmLoader) -> Contract: +def create_contract_address(user: Union[Caller, bytes], evm_loader: EvmLoader, chain_id=CHAIN_ID) -> Contract: # Create contract address from (caller_address, nonce) - user_nonce = evm_loader.get_neon_nonce(user.eth_address) + if isinstance(user, Caller): + user = user.eth_address + user_nonce = evm_loader.get_neon_nonce(user, chain_id) contract_eth_address = ( - keccak.new(digest_bits=256).update(pack([user.eth_address, user_nonce or None])).digest()[-20:] + keccak.new(digest_bits=256).update(pack([user, user_nonce or None])).digest()[-20:] ) contract_solana_address, _ = evm_loader.ether2program(contract_eth_address) - contract_neon_address = evm_loader.ether2balance(contract_eth_address) + contract_neon_address = evm_loader.ether2balance(contract_eth_address, chain_id) print(f"Contract addresses: " f" eth {contract_eth_address.hex()}, " f" solana {contract_solana_address}") @@ -38,6 +40,7 @@ def make_eth_transaction( type_=None, gas_price=0 ): + nonce = evm_loader.get_neon_nonce(caller.eth_address) tx = {"to": to_addr, "value": value, "gas": gas, "gasPrice": gas_price, "nonce": nonce} diff --git a/integration/tests/neon_evm/utils/neon_api_client.py b/integration/tests/neon_evm/utils/neon_api_client.py index aef1203d88..651a7f0a6d 100644 --- a/integration/tests/neon_evm/utils/neon_api_client.py +++ b/integration/tests/neon_evm/utils/neon_api_client.py @@ -1,8 +1,11 @@ import eth_abi import requests from eth_utils import abi +from solders.pubkey import Pubkey +from integration.tests.neon_evm.utils.constants import SOL_CHAIN_ID from utils.evm_loader import CHAIN_ID +from utils.models.tree_account import TreeAccount from utils.types import Caller, Contract @@ -83,3 +86,13 @@ def get_steps_count(self, from_acc, to, data): ) return result["steps_executed"] + def get_transaction_tree(self, address, nonce, chain_id=SOL_CHAIN_ID) -> TreeAccount: + if isinstance(address, Pubkey): + address = bytes(address).hex() + body = { + "origin": {"address": address, "chain_id": chain_id}, + "nonce": nonce + } + response = requests.post(url=f"{self.url}/transaction_tree", json=body, headers=self.headers).json() + return TreeAccount.from_dict(response) + diff --git a/scripts/block_time.py b/scripts/block_time.py new file mode 100644 index 0000000000..f9a552f5ab --- /dev/null +++ b/scripts/block_time.py @@ -0,0 +1,51 @@ +import os +import sys +import time +from datetime import datetime, timezone +from solana.rpc.core import RPCException + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +from utils.solana_client import SolanaClient + +sol_client = SolanaClient("https://solana-api.instantnodes.io/token-OjVeh8exYGMeFh7QKIRLsF93T4xratD6") + +expected_time = '20-10-2024 00:00:00' + +dt = datetime.strptime(expected_time, '%d-%m-%Y %H:%M:%S') +unix_time = int(time.mktime(dt.timetuple())) + +utc_unix_time = int(dt.replace(tzinfo=timezone.utc).timestamp()) + + +def get_time_for_closest_exist_slot(slot: int): + try: + time = sol_client.get_block_time(slot).value + return slot, time + except RPCException as e: + print(f"Slot {slot} is skipped {e}") + return get_time_for_closest_exist_slot(slot + 1) + + +# Given data +current_slot = sol_client.get_slot().value +current_time = sol_client.get_block_time(current_slot).value +slot_time_seconds = 0.4 +expected_timestamp = utc_unix_time + + +def get_estimated_slot_for_time(current_slot, current_time, expected_timestamp, slot_time_seconds): + time_difference_seconds = current_time - expected_timestamp + slots_passed = time_difference_seconds / slot_time_seconds + counted_slot = current_slot - int(slots_passed) + return counted_slot + + +while abs(current_time - expected_timestamp) > 1: + current_slot = get_estimated_slot_for_time(current_slot, current_time, expected_timestamp, slot_time_seconds) + current_time = get_time_for_closest_exist_slot(current_slot)[1] + + print(current_slot) + +print("Timestamp for the closest slot: ", current_time) \ No newline at end of file diff --git a/utils/consts.py b/utils/consts.py index e85a597df7..4fec13e7e9 100644 --- a/utils/consts.py +++ b/utils/consts.py @@ -2,6 +2,8 @@ from solders.pubkey import Pubkey +from utils.helpers import to_little_endian_byte + OPERATOR_KEYPAIR_PATH = "deploy/operator-keypairs" LAMPORT_PER_SOL = 1_000_000_000 ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" @@ -62,3 +64,31 @@ class InputTestConstants(Enum): "USDT": "2duuuuhNJHUYqcnZ7LKfeufeeTBgSJdftf2zM3cZV6ym", "ETH": "EwJYd3UAFAgzodVeHprB2gMQ68r4ZEbbvpoVzCZ1dGq5", } + + +class InstructionTags(bytes, Enum): + HOLDER_CREATE = b'\x24' + HOLDER_DELETE = b'\x25' + HOLDER_WRITE = b'\x26' + CREATE_MAIN_TREASURY = b'\x29' + ACCOUNT_CREATE_BALANCE = b'\x30' + DEPOSIT = b'\x31' + TRANSACTION_EXECUTE_FROM_INSTRUCTION = b'\x3D' + TRANSACTION_EXECUTE_FROM_ACCOUNT = b'\x33' + TRANSACTION_STEP_FROM_INSTRUCTION = b'\x34' + TRANSACTION_STEP_FROM_ACCOUNT = b'\x35' + TRANSACTION_STEP_FROM_ACCOUNT_NO_CHAIN_ID = b'\x36' + CANCEL = b'\x37' + TRANSACTION_EXECUTE_FROM_INSTRUCTION_WITH_SOLANA_CALL = b'\x3E' + TRANSACTION_EXECUTE_FROM_ACCOUNT_WITH_SOLANA_CALL = b'\x39' + OPERATOR_BALANCE_CREATE = b'\x3A' + OPERATOR_BALANCE_DELETE = b'\x3B' + OPERATOR_BALANCE_WITHDRAW = b'\x3C' + SCHEDULED_TRANSACTION_START_FROM_ACCOUNT = b'\x46' + SCHEDULED_TRANSACTION_START_FROM_INSTRUCTION = b'\x47' + SCHEDULED_TRANSACTION_SKIP_FROM_INSTRUCTION = b'\x4E' + SCHEDULED_TRANSACTION_SKIP_FROM_ACCOUNT = b'\x4D' + SCHEDULED_TRANSACTION_FINISH = b'\x49' + SCHEDULED_TRANSACTION_CREATE = b'\x4A' + SCHEDULED_TRANSACTION_CREATE_MULTIPLE = b'\x4B' + SCHEDULED_TRANSACTION_DESTROY = b'\x4C' diff --git a/utils/evm_loader.py b/utils/evm_loader.py index 3087d13b07..523d02ce79 100644 --- a/utils/evm_loader.py +++ b/utils/evm_loader.py @@ -5,8 +5,10 @@ import spl import typing as tp +from eth_account.signers.local import LocalAccount from eth_keys import keys as eth_keys from eth_account.datastructures import SignedTransaction +from eth_utils import keccak from solders.keypair import Keypair from solders.pubkey import Pubkey import solders.system_program as sp @@ -14,16 +16,19 @@ from solana.rpc.types import TxOpts from solana.transaction import Transaction from solders.rpc.responses import SendTransactionResp, GetTransactionResp -from solders.transaction_status import EncodedConfirmedTransactionWithStatusMeta from spl.token.instructions import get_associated_token_address, MintToParams, ApproveParams, approve from spl.token.constants import TOKEN_PROGRAM_ID +from utils.scheduled_trx import ScheduledTransaction +from utils.neon_user import NeonUser from integration.tests.neon_evm.utils.constants import ( TREASURY_POOL_SEED, NEON_TOKEN_MINT_ID, CHAIN_ID, + SOL_CHAIN_ID, ) from utils.consts import LAMPORT_PER_SOL, wSOL +from utils.helpers import ether2bytes from utils.instructions import ( TransactionWithComputeBudget, make_ExecuteTrxFromInstruction, @@ -36,8 +41,20 @@ make_DepositV03, make_wSOL, make_OperatorBalanceAccount, + make_ScheduledTransactionCreate, + make_ScheduledTransactionStartFromAccount, + make_ScheduledTransactionFinish, + make_ScheduledTransactionDestroy, + make_ScheduledTransactionStartFromInstruction, + make_ScheduledTransactionCreateMultiple, + make_ScheduledTransactionSkipFromInstruction, +) +from utils.layouts import ( + BALANCE_ACCOUNT_LAYOUT, + CONTRACT_ACCOUNT_LAYOUT, + STORAGE_CELL_LAYOUT, + OPERATOR_BALANCE_ACCOUNT_LAYOUT, ) -from utils.layouts import BALANCE_ACCOUNT_LAYOUT, CONTRACT_ACCOUNT_LAYOUT, STORAGE_CELL_LAYOUT from utils.solana_client import SolanaClient from utils.types import Caller @@ -56,7 +73,7 @@ def create_balance_account(self, ether: Union[str, bytes], sender, chain_id=CHAI trx = Transaction() trx.add( make_CreateBalanceAccount( - self.loader_id, sender.pubkey(), self.ether2bytes(ether), account_pubkey, contract_pubkey, chain_id + self.loader_id, sender.pubkey(), ether2bytes(ether), account_pubkey, contract_pubkey, chain_id ) ) self.send_tx(trx, sender) @@ -67,26 +84,17 @@ def create_treasury_pool_address(self, pool_index): [bytes(TREASURY_POOL_SEED, "utf8"), pool_index.to_bytes(4, "little")], self.loader_id )[0] - @staticmethod - def ether2bytes(ether: Union[str, bytes]): - if isinstance(ether, str): - if ether.startswith("0x"): - return bytes.fromhex(ether[2:]) - return bytes.fromhex(ether) - return ether - - @staticmethod - def ether2hex(ether: Union[str, bytes]): - if isinstance(ether, str): - if ether.startswith("0x"): - return ether[2:] - return ether - return ether.hex() - - def ether2operator_balance( - self, keypair: Keypair, ether_address: Union[str, bytes], chain_id=CHAIN_ID - ) -> Pubkey: - address_bytes = self.ether2bytes(ether_address) + def create_tree_account_address(self, neon_address, nonce, chain_id=SOL_CHAIN_ID): + chain_id_bytes = chain_id.to_bytes(8, "little") + seeds = [self.account_seed_version, b"TREE", neon_address, chain_id_bytes, nonce] + print(seeds) + return Pubkey.find_program_address(seeds, self.loader_id)[0] + + def create_get_authority_address(self): + return Pubkey.find_program_address([b"Deposit"], self.loader_id)[0] + + def ether2operator_balance(self, keypair: Keypair, ether_address: Union[str, bytes], chain_id=CHAIN_ID) -> Pubkey: + address_bytes = ether2bytes(ether_address) key = bytes(keypair.pubkey()) chain_id_bytes = chain_id.to_bytes(32, "big") return Pubkey.find_program_address( @@ -95,11 +103,12 @@ def ether2operator_balance( def get_neon_nonce(self, account: Union[str, bytes], chain_id=CHAIN_ID) -> int: solana_address = self.ether2balance(account, chain_id) - - info: bytes = self.get_solana_account_data(solana_address, BALANCE_ACCOUNT_LAYOUT.sizeof()) - layout = BALANCE_ACCOUNT_LAYOUT.parse(info) - - return layout.trx_count + if self.account_exists(solana_address): + info: bytes = self.get_solana_account_data(solana_address, BALANCE_ACCOUNT_LAYOUT.sizeof()) + layout = BALANCE_ACCOUNT_LAYOUT.parse(info) + return layout.trx_count + else: + return 0 def get_solana_account_data(self, account: Union[str, Pubkey, Keypair], expected_length: int) -> bytes: if isinstance(account, Keypair): @@ -121,6 +130,14 @@ def get_neon_balance(self, account: Union[str, bytes], chain_id=CHAIN_ID) -> int return int.from_bytes(layout.balance, byteorder="little") + def get_operator_neon_balance(self, operator: Keypair, chain_id=CHAIN_ID) -> int: + balance_address = self.get_operator_balance_pubkey(operator, chain_id) + + info: bytes = self.get_solana_account_data(balance_address, OPERATOR_BALANCE_ACCOUNT_LAYOUT.sizeof()) + layout = OPERATOR_BALANCE_ACCOUNT_LAYOUT.parse(info) + + return int.from_bytes(layout.balance, byteorder="little") + def get_contract_account_revision(self, address): account_data = self.get_solana_account_data(address, CONTRACT_ACCOUNT_LAYOUT.sizeof()) return CONTRACT_ACCOUNT_LAYOUT.parse(account_data).revision @@ -131,17 +148,22 @@ def get_data_account_revision(self, address): def write_transaction_to_holder_account( self, - signed_tx: SignedTransaction, + tx: Union[SignedTransaction, bytes], holder_account: Pubkey, operator: Keypair, ): offset = 0 receipts = [] - rest = signed_tx.rawTransaction + if isinstance(tx, SignedTransaction): + rest = tx.rawTransaction + tx_hash = tx.hash + else: + tx_hash = keccak(tx) + rest = tx while len(rest): (part, rest) = (rest[:920], rest[920:]) trx = Transaction() - trx.add(make_WriteHolder(operator.pubkey(), self.loader_id, holder_account, signed_tx.hash, offset, part)) + trx.add(make_WriteHolder(operator.pubkey(), self.loader_id, holder_account, tx_hash, offset, part)) receipts.append( self.send_transaction( trx, @@ -155,21 +177,21 @@ def write_transaction_to_holder_account( self.confirm_transaction(rcpt.value, commitment=Confirmed) def ether2program(self, ether: tp.Union[str, bytes]) -> tp.Tuple[str, int]: - items = Pubkey.find_program_address([self.account_seed_version, self.ether2bytes(ether)], self.loader_id) + items = Pubkey.find_program_address([self.account_seed_version, ether2bytes(ether)], self.loader_id) return str(items[0]), items[1] def ether2balance(self, address: tp.Union[str, bytes], chain_id=CHAIN_ID) -> Pubkey: # get public key associated with chain_id for an address - address_bytes = self.ether2bytes(address) + address_bytes = ether2bytes(address) chain_id_bytes = chain_id.to_bytes(32, "big") - return Pubkey.find_program_address( - [self.account_seed_version, address_bytes, chain_id_bytes], self.loader_id - )[0] + return Pubkey.find_program_address([self.account_seed_version, address_bytes, chain_id_bytes], self.loader_id)[ + 0 + ] - def get_operator_balance_pubkey(self, operator: Keypair): + def get_operator_balance_pubkey(self, operator: Keypair, chain_id=CHAIN_ID) -> Pubkey: operator_ether = eth_keys.PrivateKey(operator.secret()[:32]).public_key.to_canonical_address() - return self.ether2operator_balance(operator, operator_ether) + return self.ether2operator_balance(operator, operator_ether, chain_id) def execute_trx_from_instruction( self, @@ -181,7 +203,7 @@ def execute_trx_from_instruction( additional_accounts, signer: Keypair = None, system_program=sp.ID, - compute_unit_price=None + compute_unit_price=None, ) -> SendTransactionResp: signer = operator if signer is None else signer trx = TransactionWithComputeBudget(operator, compute_unit_price=compute_unit_price) @@ -197,7 +219,8 @@ def execute_trx_from_instruction( treasury_buffer, instruction.rawTransaction, additional_accounts, - system_program) + system_program, + ) ) return self.send_tx(trx, signer) @@ -298,21 +321,26 @@ def send_transaction_step_from_instruction( operator_balance_pubkey, treasury, storage_account, - instruction: SignedTransaction, + instruction: Union[SignedTransaction, bytes], additional_accounts, steps_count, signer: Keypair, system_program=sp.ID, index=0, + compute_unit_price=None, tag=0x34, - ) -> GetTransactionResp: - trx = TransactionWithComputeBudget(operator) + ) -> GetTransactionResp: + trx = TransactionWithComputeBudget(operator, compute_unit_price=compute_unit_price) + if isinstance(instruction, SignedTransaction): + raw_trx = instruction.rawTransaction + else: + raw_trx = instruction trx.add( make_PartialCallOrContinueFromRawEthereumTX( index, steps_count, - instruction.rawTransaction, + raw_trx, operator, operator_balance_pubkey, self.loader_id, @@ -320,7 +348,7 @@ def send_transaction_step_from_instruction( treasury, additional_accounts, system_program, - tag + tag, ) ) @@ -334,9 +362,11 @@ def execute_transaction_steps_from_instruction( instruction: SignedTransaction, additional_accounts, signer: Keypair = None, + compute_unit_price=None, + chain_id=CHAIN_ID, ) -> GetTransactionResp: signer = operator if signer is None else signer - operator_balance_pubkey = self.get_operator_balance_pubkey(operator) + operator_balance_pubkey = self.get_operator_balance_pubkey(operator, chain_id) index = 0 receipt = None done = False @@ -350,10 +380,10 @@ def execute_transaction_steps_from_instruction( additional_accounts, EVM_STEPS, signer, + compute_unit_price=compute_unit_price, index=index, ) index += 1 - if receipt.value.transaction.meta.err: raise AssertionError(f"Transaction failed with error: {receipt.value.transaction.meta.err}") for log in receipt.value.transaction.meta.log_messages: @@ -391,7 +421,7 @@ def send_transaction_step_from_account( treasury, additional_accounts, system_program, - tag + tag, ) ) return self.send_tx(trx, signer) @@ -403,10 +433,11 @@ def execute_transaction_steps_from_account( storage_account, additional_accounts, signer: Keypair = None, - compute_unit_price=None + compute_unit_price=None, + chain_id=CHAIN_ID, ) -> GetTransactionResp: signer = operator if signer is None else signer - operator_balance_pubkey = self.get_operator_balance_pubkey(operator) + operator_balance_pubkey = self.get_operator_balance_pubkey(operator, chain_id) index = 0 receipt = None @@ -421,7 +452,7 @@ def execute_transaction_steps_from_account( EVM_STEPS, signer, index=index, - compute_unit_price=compute_unit_price + compute_unit_price=compute_unit_price, ) index += 1 @@ -454,7 +485,7 @@ def execute_transaction_steps_from_account_no_chain_id( EVM_STEPS, signer, tag=0x36, - index=index + index=index, ) index += 1 @@ -506,7 +537,7 @@ def deposit_neon(self, operator_keypair: Keypair, ether_address: Union[str, byte ) ), make_DepositV03( - self.ether2bytes(ether_address), + ether2bytes(ether_address), CHAIN_ID, balance_pubkey, contract_pubkey, @@ -533,7 +564,7 @@ def make_new_user(self, sender: Keypair) -> Caller: caller_token = get_associated_token_address(caller_balance, NEON_TOKEN_MINT_ID) if self.get_solana_balance(caller_balance) == 0: - print(f"Create Neon account {caller_ether} for user {caller_balance}") + print(f"Create Neon account {caller_ether.hex()} for user {caller_balance}") self.create_balance_account(caller_ether, sender) print("Account solana address:", key.pubkey()) @@ -545,8 +576,10 @@ def make_new_user(self, sender: Keypair) -> Caller: def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amount, chain_id): """Transfer any token from solana to neon transaction""" - balance_pubkey = self.ether2balance(neon_account.address, chain_id) - contract_pubkey = Pubkey.from_string(self.ether2program(neon_account.address)[0]) + if isinstance(neon_account, LocalAccount): + neon_account = neon_account.address + balance_pubkey = self.ether2balance(neon_account, chain_id) + contract_pubkey = Pubkey.from_string(self.ether2program(neon_account)[0]) associated_token_address = get_associated_token_address(solana_account.pubkey(), mint) authority_pool = Pubkey.find_program_address([b"Deposit"], self.loader_id)[0] @@ -567,7 +600,7 @@ def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amo tx.add( make_DepositV03( - bytes.fromhex(neon_account.address[2:]), + bytes.fromhex(neon_account[2:]), chain_id, balance_pubkey, contract_pubkey, @@ -617,9 +650,152 @@ def deposit_neon_like_tokens_from_solana_to_neon( def create_operator_balance_account(self, operator_keypair, operator_ether, chain_id=CHAIN_ID): account = self.ether2operator_balance(operator_keypair, operator_ether, chain_id) trx = make_OperatorBalanceAccount( - operator_keypair, account, self.ether2bytes(operator_ether), chain_id, self.loader_id + operator_keypair, account, ether2bytes(operator_ether), chain_id, self.loader_id ) self.send_tx(trx, operator_keypair) - def was_called_in_tx(self, tx: EncodedConfirmedTransactionWithStatusMeta) -> bool: - return self.transaction_contains_call_to_program(tx=tx, program_id=self.loader_id) + def create_tree_account(self, neon_user: NeonUser, treasury, transaction, mint, chain_id=SOL_CHAIN_ID): + payer_nonce = self.get_neon_nonce(neon_user.neon_address, chain_id).to_bytes(8, "little") + authority_pool = self.create_get_authority_address() + tree_account = self.create_tree_account_address(neon_user.neon_address, payer_nonce, chain_id) + pool = get_associated_token_address(authority_pool, mint) + + trx = Transaction() + balance_account = self.create_balance_account(neon_user.neon_address, neon_user.solana_account, chain_id) + trx.add( + make_ScheduledTransactionCreate( + neon_user.solana_account, balance_account, treasury, tree_account, pool, transaction, self.loader_id + ) + ) + self.send_tx(trx, neon_user.solana_account) + return tree_account + + def create_tree_account_multiple( + self, neon_user, treasury, tree_account_create_data, mint, payer_nonce=None, chain_id=SOL_CHAIN_ID + ): + if not payer_nonce: + payer_nonce = self.get_neon_nonce(neon_user.neon_address, chain_id).to_bytes(8, "little") + else: + payer_nonce = payer_nonce.to_bytes(8, "little") + authority_pool = self.create_get_authority_address() + tree_account = self.create_tree_account_address(neon_user.neon_address, payer_nonce, chain_id) + pool = get_associated_token_address(authority_pool, mint) + + trx = Transaction() + balance_account = self.create_balance_account(neon_user.neon_address, neon_user.solana_account, chain_id) + trx.add( + make_ScheduledTransactionCreateMultiple( + neon_user.solana_account, + balance_account, + treasury, + tree_account, + pool, + tree_account_create_data, + self.loader_id, + ) + ) + self.send_tx(trx, neon_user.solana_account) + return tree_account + + def start_scheduled_trx_from_account( + self, index, operator, holder, tree_account, additional_accounts, chain_id=SOL_CHAIN_ID + ): + operator_balance = self.get_operator_balance_pubkey(operator, chain_id) + trx = TransactionWithComputeBudget(operator, compute_unit_price=1000000) + trx.add( + make_ScheduledTransactionStartFromAccount( + index, operator, operator_balance, self.loader_id, holder, tree_account, additional_accounts + ) + ) + return self.send_tx(trx, operator) + + def start_scheduled_trx_from_instruction( + self, neon_trx: ScheduledTransaction, operator, holder, tree_account, additional_accounts, chain_id=SOL_CHAIN_ID + ): + operator_balance = self.get_operator_balance_pubkey(operator, chain_id) + trx = TransactionWithComputeBudget(operator, compute_unit_price=1000000) + trx.add( + make_ScheduledTransactionStartFromInstruction( + neon_trx.index, + neon_trx.encode(), + holder, + tree_account, + self.loader_id, + operator, + operator_balance, + additional_accounts, + ) + ) + return self.send_tx(trx, operator) + + def execute_scheduled_trx_from_account( + self, + index, + operator, + holder, + tree_account, + treasury, + additional_accounts, + chain_id=SOL_CHAIN_ID, + compute_unit_price=None, + ): + self.start_scheduled_trx_from_account(index, operator, holder, tree_account, additional_accounts, chain_id) + return self.execute_transaction_steps_from_account( + operator, treasury, holder, additional_accounts, chain_id=chain_id, compute_unit_price=compute_unit_price + ) + + def execute_scheduled_trx_from_instruction( + self, + trx: ScheduledTransaction, + operator, + holder, + tree_account, + treasury, + additional_accounts, + chain_id=SOL_CHAIN_ID, + ): + self.start_scheduled_trx_from_instruction(trx, operator, holder, tree_account, additional_accounts, chain_id) + self.execute_transaction_steps_from_instruction( + operator, treasury, holder, trx.encode(), additional_accounts, compute_unit_price=15, chain_id=chain_id + ) + + def finish_scheduled_trx(self, operator, tree_account, holder_account, chain_id=SOL_CHAIN_ID): + trx = TransactionWithComputeBudget(operator, compute_unit_price=1000000) + operator_balance = self.get_operator_balance_pubkey(operator, chain_id) + trx.add( + make_ScheduledTransactionFinish(operator, operator_balance, self.loader_id, holder_account, tree_account) + ) + return self.send_tx(trx, operator) + + def skip_scheduled_trx_from_instruction( + self, neon_trx, operator, tree_account, holder_account, chain_id=SOL_CHAIN_ID + ): + operator_balance_pubkey = self.get_operator_balance_pubkey(operator, chain_id) + trx = TransactionWithComputeBudget(operator, compute_unit_price=1000000) + trx.add( + make_ScheduledTransactionSkipFromInstruction( + neon_trx.index, + neon_trx.encode(), + operator, + operator_balance_pubkey, + holder_account, + tree_account, + self.loader_id, + ) + ) + return self.send_tx(trx, operator) + + def destroy_tree_account(self, operator, neon_user: NeonUser, treasury, tree_account, chain_id=SOL_CHAIN_ID): + trx = Transaction() + + trx.add( + make_ScheduledTransactionDestroy( + operator, + neon_user.solana_account, + neon_user.get_balance_account(chain_id), + treasury, + tree_account, + self.loader_id, + ) + ) + return self.send_tx(trx, operator) diff --git a/utils/helpers.py b/utils/helpers.py index ab76ef7259..321103572f 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -19,7 +19,6 @@ from semantic_version import Version from solders.rpc.responses import GetTransactionResp - T = tp.TypeVar('T') @@ -32,11 +31,11 @@ def get_contract_abi(name, compiled): @allure.step("Get contract interface") def get_contract_interface( - contract: str, - version: str, - contract_name: tp.Optional[str] = None, - import_remapping: tp.Optional[dict] = None, - libraries: tp.Optional[dict] = None, + contract: str, + version: str, + contract_name: tp.Optional[str] = None, + import_remapping: tp.Optional[dict] = None, + libraries: tp.Optional[dict] = None, ): if not contract.endswith(".sol"): contract += ".sol" @@ -156,6 +155,15 @@ def get_selectors(abi_): return selectors +def get_event_signatures(abi: tp.List[tp.Dict]) -> tp.List[str]: + """Get topics as keccak256 from abi Events""" + topics = [] + for event in filter(lambda item: item["type"] == "event", abi): + input_types = ",".join(i["type"] for i in event["inputs"]) + signature = f"{event['name']}({input_types})" + topics.append(f"0x{keccak(signature.encode()).hex()}") + return topics + @allure.step("Create non-existing account address") def create_invalid_address(length=20) -> str: """Create non-existing account address""" @@ -195,6 +203,23 @@ def solana_pubkey_to_bytes32(solana_pubkey): return byte_data +def pubkey2neon_address(pubkey: Pubkey) -> bytes: + bytes_part = keccak(primitive=bytes(pubkey))[12:32] + return bytes_part + + +def to_little_endian_byte(value: int) -> bytes: + return value.to_bytes(1, "little") + + +def ether2bytes(ether: typing.Union[str, bytes]): + if isinstance(ether, str): + if ether.startswith("0x"): + return bytes.fromhex(ether[2:]) + return bytes.fromhex(ether) + return ether + + def serialize_instruction(program_id: Pubkey, instruction) -> bytes: program_id_bytes = solana_pubkey_to_bytes32(program_id) serialized = program_id_bytes + len(instruction.accounts).to_bytes(8, "little") diff --git a/utils/instructions.py b/utils/instructions.py index 61fd8a2af2..3d0883a45f 100644 --- a/utils/instructions.py +++ b/utils/instructions.py @@ -6,7 +6,7 @@ import solders.system_program as sp from solana.transaction import AccountMeta, Instruction, Transaction -from utils.consts import COMPUTE_BUDGET_ID +from utils.consts import COMPUTE_BUDGET_ID, InstructionTags from solders.system_program import ID as SYS_PROGRAM_ID from .metaplex import SYSVAR_RENT_PUBKEY from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID @@ -25,7 +25,7 @@ def request_units(operator: Keypair, units): return Instruction( program_id=COMPUTE_BUDGET_ID, accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], - data=bytes.fromhex("02") + units.to_bytes(4, "little") + data=bytes.fromhex("02") + units.to_bytes(4, "little"), ) @staticmethod @@ -33,7 +33,7 @@ def request_heap_frame(operator: Keypair, heap_frame): return Instruction( program_id=COMPUTE_BUDGET_ID, accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], - data=bytes.fromhex("01") + heap_frame.to_bytes(4, "little") + data=bytes.fromhex("01") + heap_frame.to_bytes(4, "little"), ) @staticmethod @@ -41,7 +41,7 @@ def set_compute_units_price(price, operator: Keypair): return Instruction( program_id=COMPUTE_BUDGET_ID, accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], - data=bytes.fromhex("03") + price.to_bytes(8, "little") + data=bytes.fromhex("03") + price.to_bytes(8, "little"), ) @@ -62,7 +62,7 @@ def __init__( if heap_frame: self.add(ComputeBudget.request_heap_frame(operator, heap_frame)) if compute_unit_price: - self.add(ComputeBudget.set_compute_units_price(compute_unit_price, operator)) + self.add(ComputeBudget.set_compute_units_price(compute_unit_price, operator)) def make_WriteHolder( @@ -90,7 +90,7 @@ def make_ExecuteTrxFromInstruction( message: bytes, additional_accounts: tp.List[Pubkey], system_program=sp.ID, - tag=0x3D + tag=0x3D, ): data = bytes([tag]) + treasury_buffer + message print("make_ExecuteTrxFromInstruction accounts") @@ -103,7 +103,7 @@ def make_ExecuteTrxFromInstruction( AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury_address, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True) + AccountMeta(system_program, is_signer=False, is_writable=True), ] for acc in additional_accounts: print("Additional acc ", acc) @@ -124,7 +124,7 @@ def make_ExecuteTrxFromAccount( additional_accounts: tp.List[Pubkey], additional_signers: tp.List[Keypair] = None, system_program=sp.ID, - tag=0x33 + tag=0x33, ): data = bytes([tag]) + treasury_buffer print("make_ExecuteTrxFromInstruction accounts") @@ -136,7 +136,7 @@ def make_ExecuteTrxFromAccount( AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury_address, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True) + AccountMeta(system_program, is_signer=False, is_writable=True), ] for acc in additional_accounts: print("Additional acc ", acc) @@ -161,7 +161,7 @@ def make_ExecuteTrxFromAccountDataIterativeOrContinue( treasury, additional_accounts: tp.List[Pubkey], sys_program_id=sp.ID, - tag=0x35 + tag=0x35, ): # 0x35 - TransactionStepFromAccount # 0x36 - TransactionStepFromAccountNoChainId @@ -176,7 +176,7 @@ def make_ExecuteTrxFromAccountDataIterativeOrContinue( AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(sys_program_id, is_signer=False, is_writable=True) + AccountMeta(sys_program_id, is_signer=False, is_writable=True), ] for acc in additional_accounts: @@ -199,7 +199,7 @@ def make_PartialCallOrContinueFromRawEthereumTX( treasury: TreasuryPool, additional_accounts: tp.List[Pubkey], system_program=sp.ID, - tag=0x34 # TransactionStepFromInstruction + tag=0x34, # TransactionStepFromInstruction ): data = bytes([tag]) + treasury.buffer + step_count.to_bytes(4, "little") + index.to_bytes(4, "little") + instruction @@ -208,7 +208,7 @@ def make_PartialCallOrContinueFromRawEthereumTX( AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True) + AccountMeta(system_program, is_signer=False, is_writable=True), ] for acc in additional_accounts: accounts.append( @@ -286,7 +286,7 @@ def make_CreateAssociatedTokenIdempotent(payer: Pubkey, owner: Pubkey, mint: Pub AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False) + AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), ], program_id=ASSOCIATED_TOKEN_PROGRAM_ID, ) @@ -310,7 +310,7 @@ def make_CreateBalanceAccount( AccountMeta(pubkey=sender_pubkey, is_signer=True, is_writable=True), AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), AccountMeta(pubkey=account_pubkey, is_signer=False, is_writable=True), - AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True) + AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True), ], ) @@ -350,31 +350,162 @@ def make_CreateHolderAccount(account, operator, seed, evm_loader_id): def make_wSOL(amount, solana_wallet, ata_address): tx = Transaction(fee_payer=solana_wallet) - tx.add(sp.transfer(sp.TransferParams( - from_pubkey=solana_wallet, to_pubkey=ata_address, lamports=amount) - )) + tx.add(sp.transfer(sp.TransferParams(from_pubkey=solana_wallet, to_pubkey=ata_address, lamports=amount))) tx.add(make_SyncNative(ata_address)) return tx def make_OperatorBalanceAccount(operator_keypair, operator_balance_pubkey, ether_bytes, chain_id, evm_loader_id): + tag = InstructionTags.OPERATOR_BALANCE_CREATE trx = Transaction() - trx.add(Instruction( - accounts=[ - AccountMeta(pubkey=operator_keypair.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator_balance_pubkey, is_signer=False, is_writable=True) - ], - program_id=evm_loader_id, - data=bytes.fromhex("3A") + ether_bytes + chain_id.to_bytes(8, 'little') - )) + trx.add( + Instruction( + accounts=[ + AccountMeta(pubkey=operator_keypair.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator_balance_pubkey, is_signer=False, is_writable=True), + ], + program_id=evm_loader_id, + data=tag + ether_bytes + chain_id.to_bytes(8, "little"), + ) + ) + return trx + + +def make_ScheduledTransactionCreate(signer, balance_pubkey, treasury, tree_account, pool, msg, evm_loader_id): + tag = InstructionTags.SCHEDULED_TRANSACTION_CREATE + trx = Transaction() + trx.add( + Instruction( + accounts=[ + AccountMeta(pubkey=signer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=balance_pubkey, is_signer=False, is_writable=True), + AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), + ], + program_id=evm_loader_id, + data=tag + treasury.buffer + msg, + ) + ) return trx +def make_ScheduledTransactionCreateMultiple(signer, balance_pubkey, treasury, tree_account, pool, msg, evm_loader_id): + tag = InstructionTags.SCHEDULED_TRANSACTION_CREATE_MULTIPLE + data = tag + treasury.buffer + msg + trx = Transaction() + trx.add( + Instruction( + accounts=[ + AccountMeta(pubkey=signer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=balance_pubkey, is_signer=False, is_writable=True), + AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), + ], + program_id=evm_loader_id, + data=data, + ) + ) + return trx + + +def make_ScheduledTransactionStartFromAccount( + index: int, + operator: Keypair, + operator_balance: Pubkey, + evm_loader_id: Pubkey, + holder_address: Pubkey, + tree_account: Pubkey, + additional_accounts: tp.List[Pubkey], +): + tag = InstructionTags.SCHEDULED_TRANSACTION_START_FROM_ACCOUNT + data = tag + index.to_bytes(4, "little") + accounts = [ + AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), + ] + + for acc in additional_accounts: + print("Additional acc ", acc) + accounts.append( + AccountMeta(acc, is_signer=False, is_writable=True), + ) + + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) + + +def make_ScheduledTransactionStartFromInstruction( + index, neon_trx, holder_address, tree_account, evm_loader_id, operator, operator_balance, additional_accounts +): + tag = InstructionTags.SCHEDULED_TRANSACTION_START_FROM_INSTRUCTION + data = tag + index.to_bytes(4, "little") + neon_trx + accounts = [ + AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), + ] + + for acc in additional_accounts: + print("Additional acc ", acc) + accounts.append( + AccountMeta(acc, is_signer=False, is_writable=True), + ) + + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) + + +def make_ScheduledTransactionDestroy(operator, signer, balance_account, treasury, tree_account, evm_loader_id): + data = InstructionTags.SCHEDULED_TRANSACTION_DESTROY + treasury.buffer + accounts = [ + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=balance_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=signer.pubkey(), is_signer=False, is_writable=True), + ] + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) + + +def make_ScheduledTransactionFinish( + operator: Keypair, operator_balance: Pubkey, evm_loader_id: Pubkey, holder_address: Pubkey, tree_account: Pubkey +): + data = InstructionTags.SCHEDULED_TRANSACTION_FINISH + accounts = [ + AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), + ] + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) + + +def make_ScheduledTransactionSkipFromInstruction( + index: int, neon_trx: bytes, operator: Keypair, operator_balance: Pubkey, holder_address: Pubkey, tree_account: Pubkey, evm_loader_id: Pubkey +): + data = InstructionTags.SCHEDULED_TRANSACTION_SKIP_FROM_INSTRUCTION + data += index.to_bytes(4, "little") + neon_trx + accounts = [ + AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=tree_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True) + ] + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) + + def get_compute_unit_price_eip_1559( - gas_price: int, - max_priority_fee_per_gas: int, + gas_price: int, + max_priority_fee_per_gas: int, ) -> int: """ :return: micro lamports diff --git a/utils/layouts.py b/utils/layouts.py index c3ba75edb9..28a7446047 100644 --- a/utils/layouts.py +++ b/utils/layouts.py @@ -3,17 +3,22 @@ HOLDER_ACCOUNT_INFO_LAYOUT = Struct( "tag" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "owner" / Bytes(32), "hash" / Bytes(32), "len" / Int64ul, - "heap_offset" / Int64ul + # Technicall, heap_offset is not a part of Holder's Header and is located at a fixed + # memory location after the Header with some padding. + # But, since heap_offset is located strictly before the Buffer, we can + # treat at as a part of the Header. + #"_padding" / Bytes(24), + "heap_offset" / Int64ul, ) FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT = Struct( "tag" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "owner" / Bytes(32), "hash" / Bytes(32), ) @@ -21,7 +26,7 @@ CONTRACT_ACCOUNT_LAYOUT = Struct( "type" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "address" / Bytes(20), "chain_id" / Int64ul, "generation" / Int32ul, @@ -30,7 +35,7 @@ BALANCE_ACCOUNT_LAYOUT = Struct( "type" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "address" / Bytes(20), "chain_id" / Int64ul, "trx_count" / Int64ul, @@ -39,7 +44,7 @@ OPERATOR_BALANCE_ACCOUNT_LAYOUT = Struct( "type" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "owner" / Bytes(32), "address" / Bytes(20), "chain_id" / Int64ul, @@ -48,7 +53,7 @@ STORAGE_CELL_LAYOUT = Struct( "type" / Int8ul, - "header_version"/ Int8ul, + "header_version" / Int8ul, "revision" / Int32ul, ) diff --git a/utils/models/result.py b/utils/models/result.py index b854e35551..0e26ca3f0f 100644 --- a/utils/models/result.py +++ b/utils/models/result.py @@ -234,14 +234,18 @@ class ReceiptDetails(ForbidExtra): type: HexString status: tp.Optional[HexString] = None root: tp.Optional[HexString] = None + scheduledParentTransactionHashes: tp.Optional[List[HexString]] = None + scheduledChildTransactionHashes: tp.Optional[List[HexString]] = None + @model_validator(mode="before") @classmethod def check_status(cls, values): if values.get("status") is None and values.get("root") is None: raise ValueError("Either status or root must be present") - if values.get("status") is not None and values.get("root") is not None: - raise ValueError("Either status or root must be present") + # TODO: refactor + # if values.get("status") is not None and values.get("root") is not None: + # raise ValueError("Either status or root must be present") return values @@ -298,6 +302,7 @@ class NeonReceiptDetails(ForbidExtra): gasUsed: HexString cumulativeGasUsed: HexString contractAddress: Union[HexString, None] + root: HexString status: HexString logsBloom: HexString logs: Union[List[NeonGetLogsDetails], List] @@ -310,6 +315,8 @@ class NeonReceiptDetails(ForbidExtra): neonIsCanceled: bool solanaTransactions: List[SolanaTransaction] neonCosts: List[NeonCostsDetails] + scheduledParentTransactionHashes: List[HexString] + scheduledChildTransactionHashes: List[HexString] class NeonGetTransactionResult(EthResult): diff --git a/utils/models/tree_account.py b/utils/models/tree_account.py new file mode 100644 index 0000000000..2282585917 --- /dev/null +++ b/utils/models/tree_account.py @@ -0,0 +1,77 @@ +import json +from typing import List, Dict, Any +from dataclasses import dataclass + +@dataclass +class TreeAccountTransaction: + status: str + result_hash: str + transaction_hash: str + gas_limit: str + value: str + child_transaction: int + success_execute_limit: int + parent_count: int + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'TreeAccountTransaction': + return cls( + status=data['status'], + result_hash=data['result_hash'], + transaction_hash=data['transaction_hash'], + gas_limit=data['gas_limit'], + value=data['value'], + child_transaction=data['child_transaction'], + success_execute_limit=data['success_execute_limit'], + parent_count=data['parent_count'] + ) + + def is_successful(self) -> bool: + return self.status == 'Success' + + def is_failed(self) -> bool: + return self.status == 'Failed' + + def is_skipped(self) -> bool: + return self.status == 'Skipped' + +@dataclass +class TreeAccount: + result: str + status: str + pubkey: str + payer: str + last_slot: int + chain_id: int + max_fee_per_gas: str + max_priority_fee_per_gas: str + balance: int + last_index: int + transactions: List[TreeAccountTransaction] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'TreeAccount': + transactions = [TreeAccountTransaction.from_dict(tx) for tx in data['value']['transactions']] + value = data['value'] + return cls( + result=data['result'], + status=value['status'], + pubkey=value['pubkey'], + payer=value['payer'], + last_slot=value['last_slot'], + chain_id=value['chain_id'], + max_fee_per_gas=value['max_fee_per_gas'], + max_priority_fee_per_gas=value['max_priority_fee_per_gas'], + balance=int(value['balance'], 16), + last_index=value['last_index'], + transactions=transactions + ) + + def all_transactions_successful(self) -> bool: + return all(tx.status == 'Success' for tx in self.transactions) + + def get_transaction_count(self) -> int: + return len(self.transactions) + + def get_transaction_statuses(self) -> Dict[str, str]: + return {tx.transaction_hash: tx.status for tx in self.transactions} \ No newline at end of file diff --git a/utils/neon_user.py b/utils/neon_user.py new file mode 100644 index 0000000000..cca82c40e8 --- /dev/null +++ b/utils/neon_user.py @@ -0,0 +1,24 @@ +import web3 +from solders.keypair import Keypair +from solders.pubkey import Pubkey + +from integration.tests.neon_evm.utils.constants import CHAIN_ID, ACCOUNT_SEED_VERSION, EVM_LOADER +from utils.helpers import pubkey2neon_address + + +class NeonUser: + solana_account: Keypair + neon_address: bytes + checksum_address: str + + def __init__(self): + self.solana_account = Keypair() + self.neon_address = pubkey2neon_address(self.solana_account.pubkey()) + self.checksum_address = web3.Web3.to_checksum_address(self.neon_address) + + def get_balance_account(self, chain_id=CHAIN_ID): + chain_id_bytes = chain_id.to_bytes(32, "big") + return Pubkey.find_program_address( + [ACCOUNT_SEED_VERSION, self.neon_address, chain_id_bytes], Pubkey.from_string(EVM_LOADER) + )[0] + diff --git a/utils/scheduled_trx.py b/utils/scheduled_trx.py new file mode 100644 index 0000000000..73b2ab633f --- /dev/null +++ b/utils/scheduled_trx.py @@ -0,0 +1,95 @@ +import rlp +from eth_utils import keccak, to_bytes +from rlp.sedes import big_endian_int, binary, Binary +import typing as tp + + +class ScheduledTxRLP(rlp.Serializable): + fields = [ + ("payer", Binary(min_length=20, max_length=20, allow_empty=False)), + ("sender", Binary(min_length=20, max_length=20, allow_empty=True)), + ("nonce", big_endian_int), + ("index", big_endian_int), + ("intent", binary), + ("intent_call_data", binary), + ("target", Binary(min_length=20, max_length=20, allow_empty=True)), + ("call_data", binary), + ("value", big_endian_int), + ("chain_id", big_endian_int), + ("gas_limit", big_endian_int), + ("max_fee_per_gas", big_endian_int), + ("max_priority_fee_per_gas", big_endian_int), + ] + + +class ScheduledTransaction: + DEFAULTS = { + "intent": b"", + "intent_call_data": b"", + "call_data": b"", + "value": 0, + "chain_id": 112, + "gas_limit": 3000000, + "max_fee_per_gas": 3000000000, + "max_priority_fee_per_gas": 15, + } + + FIELD_NAMES = [ + "payer", "sender", "nonce", "index", "intent", "intent_call_data", "target", + "call_data", "value", "chain_id", "gas_limit", "max_fee_per_gas", "max_priority_fee_per_gas" + ] + + def __init__(self, payer: tp.Union[bytes, str], sender, nonce, index, target: tp.Union[bytes, str, None], **kwargs): + self.payer = payer if isinstance(payer, bytes) else to_bytes(hexstr=payer[2:]) + + self.sender = sender or b"" + self.nonce = nonce + self.index = index + if target: + self.target = target if isinstance(target, bytes) else to_bytes(hexstr=target[2:]) + else: + self.target = b"" + for field, default_value in self.DEFAULTS.items(): + setattr(self, field, kwargs.get(field, default_value)) + + def encode(self): + tx_data = {field: getattr(self, field) for field in self.FIELD_NAMES} + tx = ScheduledTxRLP(**tx_data) + type_byte, sub_type_byte = 0x7F, 0x01 + return bytes([type_byte, sub_type_byte]) + rlp.encode(tx) + + def to_dict(self): + return {field: getattr(self, field) for field in self.FIELD_NAMES} + + def hash(self): + return keccak(self.encode()) + def get_serialized_node(self, child_index, success_limit): + """ + Serialize and return the node as bytes with the following layout: + - gas_limit: 32 bytes + - value: 32 bytes + - child_index: 2 bytes + - success_limit: 2 bytes + - tx_hash: 32 bytes + """ + gas_limit_bytes = self.gas_limit.to_bytes(32, byteorder="big") + value_bytes = self.value.to_bytes(32, byteorder="big") + child_index_bytes = child_index.to_bytes(2, byteorder="little") + success_limit_bytes = success_limit.to_bytes(2, byteorder="little") + tx_hash_bytes = self.hash() + + return gas_limit_bytes + value_bytes + child_index_bytes + success_limit_bytes + tx_hash_bytes + + +class CreateTreeAccMultipleData: + def __init__(self, nonce, max_fee_per_gas=3000000000, max_priority_fee_per_gas=15): + self.nonce = nonce.to_bytes(8, byteorder="big") + self.max_fee_per_gas = max_fee_per_gas.to_bytes(32, byteorder="big") + self.max_priority_fee_per_gas = max_priority_fee_per_gas.to_bytes(32, byteorder="big") + self.data = self.nonce + self.max_fee_per_gas + self.max_priority_fee_per_gas + + def add_trx(self, trx, child_index, success_limit): + self.data += trx.get_serialized_node(child_index, success_limit) + + def get_data(self): + return self.data diff --git a/utils/solana_client.py b/utils/solana_client.py index 3b54a30a39..6dfcf4e468 100644 --- a/utils/solana_client.py +++ b/utils/solana_client.py @@ -60,13 +60,6 @@ def send_sol(self, from_: Keypair, to: Pubkey, amount_lamports: int): ) self.send_tx_and_check_status_ok(tx, from_) - @staticmethod - def ether2bytes(ether: tp.Union[str, bytes]): - if isinstance(ether, str): - if ether.startswith("0x"): - return bytes.fromhex(ether[2:]) - return bytes.fromhex(ether) - return ether def get_erc_auth_address(self, neon_account_address: str, token_address: str, evm_loader_id: str): neon_account_addressbytes = bytes(12) + bytes.fromhex(neon_account_address[2:]) @@ -133,7 +126,7 @@ def wait_transaction(self, tx): def account_exists(self, account_address: Pubkey) -> bool: try: - account_info = self.get_account_info(account_address) + account_info = self.get_account_info(account_address, commitment=Confirmed) if account_info.value is not None: return True else: diff --git a/utils/stats_collector.py b/utils/stats_collector.py index ab3c7ee3c3..34fa9e1212 100644 --- a/utils/stats_collector.py +++ b/utils/stats_collector.py @@ -52,7 +52,7 @@ def wrapper(*args, **kwargs) -> TxReceipt: stack = inspect.stack() if is_called_from_test_marked_for_collection(stack=stack): if (isinstance(result, (dict, AttributeDict)) and - set(result.keys()).issubset(TxReceipt.__required_keys__)): + set(TxReceipt.__required_keys__).issubset(result.keys())): receipt = result else: try: diff --git a/utils/web3client.py b/utils/web3client.py index 2f9eb5ce10..36377d0d1e 100644 --- a/utils/web3client.py +++ b/utils/web3client.py @@ -1,7 +1,5 @@ import json import pathlib -import sys -import time import typing as tp from decimal import Decimal @@ -15,6 +13,7 @@ from eth_typing import BlockIdentifier from web3.exceptions import TransactionNotFound +from utils.scheduled_trx import ScheduledTransaction from utils.types import TransactionType from utils import helpers from utils.consts import InputTestConstants, Unit @@ -263,6 +262,28 @@ def send_transaction( signature = self._web3.eth.send_raw_transaction(instruction_tx.rawTransaction) return self._web3.eth.wait_for_transaction_receipt(signature, timeout=timeout) + def send_scheduled_transaction( + self, + trx: ScheduledTransaction, + check_result: bool = True, + ): + resp = requests.post( + self._proxy_url, + json={ + "jsonrpc": "2.0", + "method": "neon_sendRawScheduledTransaction", + "params": [trx.encode().hex()], + "id": 0, + }, + ).json() + if check_result: + assert "result" in resp, f"Failed to send scheduled transaction: {resp}" + return resp + + def send_all_scheduled_transactions(self, raw_transactions: tp.List[ScheduledTransaction]): + for trx in raw_transactions: + self.send_scheduled_transaction(trx) + @allure.step("Create raw transaction EIP-1559") def make_raw_tx_eip_1559( self, @@ -572,6 +593,19 @@ def is_trx_iterative(self, trx_hash: str) -> bool: ).json() return len(resp["result"]) > 1 + def get_pending_transactions(self, user_address: str) -> str: + resp = requests.post( + self._proxy_url, + json={ + "jsonrpc": "2.0", + "method": "neon_getPendingTransactions", + "params": [user_address], + "id": 0, + }, + ).json() + assert "result" in resp, f"Failed to get pending transactions: {resp}" + return resp["result"] + class NeonChainWeb3Client(Web3Client): def __init__(