From b5bc935218ca7dd3a4260961dcf1dd038c3e4d43 Mon Sep 17 00:00:00 2001 From: Vasiliy Zaznobin Date: Tue, 21 Dec 2021 18:40:10 +0300 Subject: [PATCH 01/14] 0.5.2-dev --- .buildkite/steps/build-image.sh | 2 +- Dockerfile | 2 +- proxy/plugin/solana_rest_api.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.buildkite/steps/build-image.sh b/.buildkite/steps/build-image.sh index 104ffc621..a67344b9f 100755 --- a/.buildkite/steps/build-image.sh +++ b/.buildkite/steps/build-image.sh @@ -4,7 +4,7 @@ set -euo pipefail REVISION=$(git rev-parse HEAD) set ${SOLANA_REVISION:=v1.7.9-testnet} -set ${EVM_LOADER_REVISION:=stable} +set ${EVM_LOADER_REVISION:=latest} # Refreshing neonlabsorg/solana:latest image is required to run .buildkite/steps/build-image.sh locally docker pull neonlabsorg/solana:${SOLANA_REVISION} diff --git a/Dockerfile b/Dockerfile index 19cbbe0e1..a1abf84f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG SOLANA_REVISION=v1.7.9-testnet -ARG EVM_LOADER_REVISION=stable +ARG EVM_LOADER_REVISION=latest FROM neonlabsorg/solana:${SOLANA_REVISION} AS cli diff --git a/proxy/plugin/solana_rest_api.py b/proxy/plugin/solana_rest_api.py index b1f34b036..854d449e4 100644 --- a/proxy/plugin/solana_rest_api.py +++ b/proxy/plugin/solana_rest_api.py @@ -46,7 +46,7 @@ modelInstanceLock = threading.Lock() modelInstance = None -NEON_PROXY_PKG_VERSION = '0.5.1' +NEON_PROXY_PKG_VERSION = '0.5.2-dev' NEON_PROXY_REVISION = 'NEON_PROXY_REVISION_TO_BE_REPLACED' class EthereumModel: From 09624a6254ae051b0100d270b4810ae966e91b4d Mon Sep 17 00:00:00 2001 From: Vasiliy Zaznobin <82812108+vasiliy-zaznobin@users.noreply.github.com> Date: Tue, 21 Dec 2021 19:19:33 +0300 Subject: [PATCH 02/14] Introduced a metric "resp_time_ms" to understand how long the neon-proxy make a response (#401) * Introduced a metric "resp_time_ms" to understand how long the neon-proxy make a response #401 --- proxy/plugin/solana_rest_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/proxy/plugin/solana_rest_api.py b/proxy/plugin/solana_rest_api.py index a09e3585b..f658b5283 100644 --- a/proxy/plugin/solana_rest_api.py +++ b/proxy/plugin/solana_rest_api.py @@ -17,6 +17,7 @@ import threading import traceback import unittest +import time from ..common.utils import build_http_response from ..http.codes import httpStatusCodes @@ -618,7 +619,7 @@ def handle_request(self, request: HttpParser) -> None: b'Access-Control-Max-Age': b'86400' }))) return - + start_time = time.time() logger.debug('<<< %s 0x%x %s', threading.get_ident(), id(self.model), request.body.decode('utf8')) response = None @@ -639,8 +640,10 @@ def handle_request(self, request: HttpParser) -> None: traceback.print_exc() response = {'jsonrpc': '2.0', 'error': {'code': -32000, 'message': str(err)}} - logger.debug('>>> %s 0x%0x %s %s', threading.get_ident(), id(self.model), json.dumps(response), - request['method'] if 'method' in request else '---') + resp_time_ms = (time.time() - start_time)*1000 # convert this into milliseconds + logger.debug('>>> %s 0x%0x %s %s resp_time_ms= %s', threading.get_ident(), id(self.model), json.dumps(response), + request.get('method', '---'), + resp_time_ms) self.client.queue(memoryview(build_http_response( httpStatusCodes.OK, body=json.dumps(response).encode('utf8'), From 5ed6bb7112a0d144e32f3b9904b037a278fe0534 Mon Sep 17 00:00:00 2001 From: Rozhkov Dmitrii Date: Wed, 22 Dec 2021 13:52:36 +0500 Subject: [PATCH 03/14] #299 emulate solidity raise an exception (#368) --- proxy/common_neon/emulator_interactor.py | 56 +++++++++--- proxy/plugin/solana_rest_api.py | 1 + ....py => test_airdropping_with_no_faucet.py} | 40 +++------ proxy/testing/test_contract_reverting.py | 90 +++++++++++++++++++ proxy/testing/testing_helpers.py | 47 ++++++++++ 5 files changed, 192 insertions(+), 42 deletions(-) rename proxy/testing/{test_airdropping_eth_accounts.py => test_airdropping_with_no_faucet.py} (64%) create mode 100644 proxy/testing/test_contract_reverting.py create mode 100644 proxy/testing/testing_helpers.py diff --git a/proxy/common_neon/emulator_interactor.py b/proxy/common_neon/emulator_interactor.py index 7f5897f4d..b8d4720d6 100644 --- a/proxy/common_neon/emulator_interactor.py +++ b/proxy/common_neon/emulator_interactor.py @@ -1,31 +1,63 @@ import json import logging +from typing import Optional, Dict, Any from .errors import EthereumError from ..environment import neon_cli - logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) def call_emulated(contract_id, caller_id, data=None, value=None): output = emulator(contract_id, caller_id, data, value) - logger.debug("call_emulated %s %s %s %s return %s", contract_id, caller_id, data, value, output) + logger.debug(f"Call emulated. contract_id: {contract_id}, caller_id: {caller_id}, data: {data}, value: {value}, return: {output}") result = json.loads(output) + check_emulated_exit_status(result) + return result + + +def check_emulated_exit_status(result: Dict[str, Any]): exit_status = result['exit_status'] if exit_status == 'revert': - result_value = result['result'] - if len(result_value) < 8 or result_value[:8] != '08c379a0': - raise EthereumError(code=3, message='execution reverted') - - offset = int(result_value[8:8+64], 16) - length = int(result_value[8+64:8+64+64], 16) - message = str(bytes.fromhex(result_value[8+offset*2+64:8+offset*2+64+length*2]), 'utf8') - raise EthereumError(code=3, message='execution reverted: '+message, data='0x'+result_value) - if result["exit_status"] != "succeed": + revert_data = result['result'] + logger.debug(f"Got revert call emulated result with data: {revert_data}") + result_value = decode_revert_message(revert_data) + if result_value is None: + raise EthereumError(code=3, message='') + else: + raise EthereumError(code=3, message='execution reverted: ' + result_value, data='0x' + result_value) + + if exit_status != "succeed": + logger.debug(f"Got not succeed emulate exit_status: {exit_status}") raise Exception("evm emulator error ", result) - return result + + +def decode_revert_message(data: str) -> Optional[str]: + data_len = len(data) + if data_len == 0: + return None + + if data_len < 8: + raise Exception(f"Too less bytes to decode revert signature: {data_len}, data: 0x{data}") + + if data[:8] != '08c379a0': + logger.debug(f"Failed to decode revert_message, unknown revert signature: {data[:8]}") + return None + + if data_len < 8 + 64: + raise Exception(f"Too less bytes to decode revert msg offset: {data_len}, data: 0x{data}") + offset = int(data[8:8 + 64], 16) * 2 + + if data_len < 8 + offset + 64: + raise Exception(f"Too less bytes to decode revert msg len: {data_len}, data: 0x{data}") + length = int(data[8 + offset:8 + offset + 64], 16) * 2 + + if data_len < 8 + offset + 64 + length: + raise Exception(f"Too less bytes to decode revert msg: {data_len}, data: 0x{data}") + + message = str(bytes.fromhex(data[8 + offset + 64:8 + offset + 64 + length]), 'utf8') + return message def emulator(contract, sender, data, value): diff --git a/proxy/plugin/solana_rest_api.py b/proxy/plugin/solana_rest_api.py index 95bc9c913..983a8958b 100644 --- a/proxy/plugin/solana_rest_api.py +++ b/proxy/plugin/solana_rest_api.py @@ -50,6 +50,7 @@ NEON_PROXY_PKG_VERSION = '0.5.2-dev' NEON_PROXY_REVISION = 'NEON_PROXY_REVISION_TO_BE_REPLACED' + class EthereumModel: def __init__(self): self.signer = self.get_solana_account() diff --git a/proxy/testing/test_airdropping_eth_accounts.py b/proxy/testing/test_airdropping_with_no_faucet.py similarity index 64% rename from proxy/testing/test_airdropping_eth_accounts.py rename to proxy/testing/test_airdropping_with_no_faucet.py index 2c7f5e4a7..036ec909f 100644 --- a/proxy/testing/test_airdropping_eth_accounts.py +++ b/proxy/testing/test_airdropping_with_no_faucet.py @@ -1,34 +1,29 @@ import unittest import os -import solcx import eth_account import eth_typing import eth_utils -from eth_account.account import LocalAccount -from web3 import Web3, eth as web3_eth +from eth_account.account import LocalAccount from solana.rpc.api import Client as SolanaClient -from ..plugin.solana_rest_api import EthereumModel -from ..plugin.solana_rest_api_tools import get_token_balance_gwei -from ..common_neon.address import ether2program +from ..plugin.solana_rest_api_tools import get_token_balance_gwei, ether2program +from .testing_helpers import SolidityContractDeployer class TestAirdroppingEthAccounts(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - cls._EVM_LOADER_ID = os.environ.get("EVM_LOADER") new_user_airdrop_amount = int(os.environ.get("NEW_USER_AIRDROP_AMOUNT", "0")) cls._EXPECTED_BALANCE_WEI = eth_utils.to_wei(new_user_airdrop_amount, 'ether') - cls._MINIMAL_GAS_PRICE = int(os.environ.get("MINIMAL_GAS_PRICE", 1)) * eth_utils.denoms.gwei - proxy_url = os.environ.get('PROXY_URL', 'http://localhost:9090/solana') - cls._web3 = Web3(Web3.HTTPProvider(proxy_url)) + cls._contract_deployer = SolidityContractDeployer() + cls._web3 = cls._contract_deployer.web3 + solana_url = os.environ.get("SOLANA_URL", "http://localhost:8899") cls._solana_client = SolanaClient(solana_url) - cls._host_solana_account = EthereumModel.get_solana_account() def test_airdrop_on_get_balance(self): account: LocalAccount = eth_account.account.Account.create() @@ -38,13 +33,13 @@ def test_airdrop_on_get_balance(self): def test_airdrop_on_deploy(self): contract_owner: LocalAccount = self._web3.eth.account.create() - contract = self._compile_and_deploy_contract(contract_owner, self._CONTRACT_STORAGE_SOURCE) + contract = self._contract_deployer.compile_and_deploy_contract(contract_owner, self._CONTRACT_STORAGE_SOURCE) actual_balance_wei = self._get_balance_wei(contract.address) self.assertEqual(self._EXPECTED_BALANCE_WEI, actual_balance_wei) def test_airdrop_onto_wrapped_new_address(self): contract_owner: LocalAccount = self._web3.eth.account.create() - contract = self._compile_and_deploy_contract(contract_owner, self._WRAPPER_CONTRACT_STORAGE_SOURCE) + contract = self._contract_deployer.compile_and_deploy_contract(contract_owner, self._WRAPPER_CONTRACT_STORAGE_SOURCE) nested_contract_address = contract.functions.getNested().call() nested_actual_balance = self._get_balance_wei(nested_contract_address) wrapper_actual_balance = self._get_balance_wei(contract.address) @@ -53,28 +48,13 @@ def test_airdrop_onto_wrapped_new_address(self): def test_airdrop_on_deploy_estimation(self): owner_eth_account: LocalAccount = self._web3.eth.account.create() - compile_result = solcx.compile_source(self._CONTRACT_STORAGE_SOURCE) - _, contract_interface = compile_result.popitem() - contract_data = contract_interface.get("bin") + compiled_info = self._contract_deployer.compile_contract(self._CONTRACT_STORAGE_SOURCE) + contract_data = compiled_info.contract_interface.get("bin") self.assertIsNotNone(contract_data) self._web3.eth.estimate_gas({"from": owner_eth_account.address, "data": contract_data}) owner_balance = self._get_balance_wei(owner_eth_account.address) self.assertEqual(self._EXPECTED_BALANCE_WEI, owner_balance) - def _compile_and_deploy_contract(self, contract_owner: LocalAccount, source: str) -> web3_eth.Contract: - compile_result = solcx.compile_source(source) - contract_id, contract_interface = compile_result.popitem() - contract = self._web3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin']) - nonce = self._web3.eth.get_transaction_count(contract_owner.address) - chain_id = self._web3.eth.chain_id - trx_signed = self._web3.eth.account.sign_transaction( - dict(nonce=nonce, chainId=chain_id, gas=987654321, gasPrice=self._MINIMAL_GAS_PRICE, to='', value=0, data=contract.bytecode), - contract_owner.key) - trx_hash = self._web3.eth.send_raw_transaction(trx_signed.rawTransaction) - trx_receipt = self._web3.eth.wait_for_transaction_receipt(trx_hash) - contract = self._web3.eth.contract(address=trx_receipt.contractAddress, abi=contract.abi) - return contract - def _get_balance_wei(self, eth_account: str) -> int: token_owner_account, nonce = ether2program(eth_account) balance = get_token_balance_gwei(self._solana_client, token_owner_account) diff --git a/proxy/testing/test_contract_reverting.py b/proxy/testing/test_contract_reverting.py new file mode 100644 index 000000000..cf54e73b9 --- /dev/null +++ b/proxy/testing/test_contract_reverting.py @@ -0,0 +1,90 @@ +import unittest +import os + +import eth_utils +from web3 import exceptions as web3_exceptions +from solana.rpc.api import Client as SolanaClient +from eth_account.account import LocalAccount + +from .testing_helpers import SolidityContractDeployer +from ..common_neon.emulator_interactor import decode_revert_message + + +class TestContractReverting(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls._contract_deployer = SolidityContractDeployer() + cls._web3 = cls._contract_deployer.web3 + + new_user_airdrop_amount = int(os.environ.get("NEW_USER_AIRDROP_AMOUNT", "0")) + cls._EXPECTED_BALANCE_WEI = eth_utils.to_wei(new_user_airdrop_amount, 'ether') + + solana_url = os.environ.get("SOLANA_URL", "http://localhost:8899") + cls._solana_client = SolanaClient(solana_url) + + def test_revert_message_decoding(self): + revert_message = decode_revert_message(self._STRING_BASED_REVERT_DATA) + self.assertEqual(revert_message, "Not enough Ether provided.") + + _STRING_BASED_REVERT_DATA = "08c379a0" \ + "0000000000000000000000000000000000000000000000000000000000000020" \ + "000000000000000000000000000000000000000000000000000000000000001a" \ + "4e6f7420656e6f7567682045746865722070726f76696465642e000000000000" + + def test_constructor_raises_string_based_error(self): + compiled_info = self._contract_deployer.compile_contract(self._CONTRACT_CONSTRUCTOR_STRING_BASED_REVERT) + with self.assertRaises(web3_exceptions.ContractLogicError) as cm: + compiled_info.contract.constructor([]).buildTransaction() + self.assertEqual("execution reverted: ListConstructable: empty list", str(cm.exception)) + + _CONTRACT_CONSTRUCTOR_STRING_BASED_REVERT = ''' + pragma solidity >=0.7.0 <0.9.0; + contract ArrConstructable { + constructor(uint256[] memory vector_) payable { + require(vector_.length > 0, "ListConstructable: empty list"); + } + } + ''' + + def test_constructor_raises_no_argument_error(self): + compiled_info = self._contract_deployer.compile_contract(self._CONTRACT_CONSTRUCTOR_REVERT) + with self.assertRaises(web3_exceptions.ContractLogicError) as cm: + compiled_info.contract.constructor([]).buildTransaction() + self.assertEqual("", str(cm.exception)) + + _CONTRACT_CONSTRUCTOR_REVERT = ''' + pragma solidity >=0.7.0 <0.9.0; + contract ArrConstructable { + constructor(uint256[] memory vector_) payable { + require(vector_.length > 0); + } + } + ''' + + def test_method_raises_string_based_error(self): + contract_owner: LocalAccount = self._web3.eth.account.create() + contract = self._contract_deployer.compile_and_deploy_contract(contract_owner, self._CONTRACT_METHOD_STRING_BASED_REVERT) + with self.assertRaises(web3_exceptions.ContractLogicError) as cm: + contract.functions.do_string_based_revert().call() + self.assertEqual("execution reverted: Predefined revert happened", str(cm.exception)) + + def test_method_raises_trivial_error(self): + contract_owner: LocalAccount = self._web3.eth.account.create() + contract = self._contract_deployer.compile_and_deploy_contract(contract_owner, self._CONTRACT_METHOD_STRING_BASED_REVERT) + with self.assertRaises(web3_exceptions.ContractLogicError) as cm: + contract.functions.do_trivial_revert().call() + self.assertEqual("", str(cm.exception)) + + _CONTRACT_METHOD_STRING_BASED_REVERT = ''' + pragma solidity >=0.7.0 <0.9.0; + contract ArrConstructable { + function do_string_based_revert() public view returns (uint256) { + require(false, "Predefined revert happened"); + } + function do_trivial_revert() public view returns (uint256) { + require(false); + } + } + ''' + diff --git a/proxy/testing/testing_helpers.py b/proxy/testing/testing_helpers.py new file mode 100644 index 000000000..e9c57c257 --- /dev/null +++ b/proxy/testing/testing_helpers.py @@ -0,0 +1,47 @@ +import dataclasses +import os +import solcx +from eth_account.account import LocalAccount +from web3 import Web3, eth as web3_eth +import eth_utils +from typing import Union, Type, Any, Dict + + +@dataclasses.dataclass +class ContractCompiledInfo: + contract_interface: Dict + contract: web3_eth.Contract + + +class SolidityContractDeployer: + + _CONTRACT_TYPE = Union[Type[web3_eth.Contract], web3_eth.Contract] + + def __init__(self): + proxy_url = os.environ.get('PROXY_URL', 'http://localhost:9090/solana') + self._web3 = Web3(Web3.HTTPProvider(proxy_url)) + + def compile_contract(self, solidity_source_code: str) -> ContractCompiledInfo: + """Returns tuple of """ + compile_result = solcx.compile_source(solidity_source_code) + _, contract_interface = compile_result.popitem() + contract = self._web3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin']) + return ContractCompiledInfo(contract_interface, contract) + + def compile_and_deploy_contract(self, contract_owner: LocalAccount, solidity_source_code: str) -> _CONTRACT_TYPE: + compiled_info = self.compile_contract(solidity_source_code) + contract = compiled_info.contract + nonce = self._web3.eth.get_transaction_count(contract_owner.address) + chain_id = self._web3.eth.chain_id + minimal_gas_price = int(os.environ.get("MINIMAL_GAS_PRICE", 1)) * eth_utils.denoms.gwei + trx_signed = self._web3.eth.account.sign_transaction( + dict(nonce=nonce, chainId=chain_id, gas=987654321, gasPrice=minimal_gas_price, to='', value=0, data=contract.bytecode), + contract_owner.key) + trx_hash = self._web3.eth.send_raw_transaction(trx_signed.rawTransaction) + trx_receipt = self._web3.eth.wait_for_transaction_receipt(trx_hash) + contract = self._web3.eth.contract(address=trx_receipt.contractAddress, abi=contract.abi) + return contract + + @property + def web3(self) -> Web3: + return self._web3 From 05cad741a2ad00e5287ccffe50815f9adea61d89 Mon Sep 17 00:00:00 2001 From: Dmitriy Borisenko Date: Wed, 22 Dec 2021 11:56:14 +0300 Subject: [PATCH 04/14] #384 fix Indexer logging error (#385) --- proxy/indexer/indexer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/indexer/indexer.py b/proxy/indexer/indexer.py index 3254bfafa..9c1e4c9fd 100644 --- a/proxy/indexer/indexer.py +++ b/proxy/indexer/indexer.py @@ -172,7 +172,7 @@ def process_receipts(self): del continue_table[storage_account] else: logger.error("Storage not found") - logger.error(eth_signature, "unknown") + logger.error(f"{eth_signature} unknown") # raise del holder_table[write_account] @@ -446,7 +446,7 @@ def submit_transaction(self, trx_struct): } self.blocks_by_hash[block_hash] = slot - logger.debug(trx_struct.eth_signature + " " + status) + logger.debug(f"{trx_struct.eth_signature} {status}") def submit_transaction_part(self, trx_struct): From d60565fe3cf98360da14d36943d87e09fa835973 Mon Sep 17 00:00:00 2001 From: Anton Lisanin Date: Wed, 22 Dec 2021 12:17:21 +0000 Subject: [PATCH 05/14] More ERC20Wrapper tests (#396) --- proxy/testing/test_erc20_wrapper_contract.py | 157 ++++++++++++++++--- 1 file changed, 137 insertions(+), 20 deletions(-) diff --git a/proxy/testing/test_erc20_wrapper_contract.py b/proxy/testing/test_erc20_wrapper_contract.py index b745c7e76..b25f0fdb0 100644 --- a/proxy/testing/test_erc20_wrapper_contract.py +++ b/proxy/testing/test_erc20_wrapper_contract.py @@ -5,7 +5,7 @@ import unittest import os import json -from solana.rpc.commitment import Confirmed, Recent +from solana.rpc.commitment import Commitment, Confirmed, Recent from solana.rpc.types import TxOpts from web3 import Web3 from solcx import install_solc @@ -14,6 +14,7 @@ from solana.rpc.api import Client as SolanaClient from solana.account import Account as SolanaAccount from solana.publickey import PublicKey +from solana.rpc.types import TokenAccountOpts from proxy.common_neon.neon_instruction import NeonInstruction @@ -45,9 +46,11 @@ function transfer(address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); + function approveSolana(bytes32 spender, uint64 value) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); + event ApprovalSolana(address indexed owner, bytes32 indexed spender, uint64 value); } ''' @@ -55,23 +58,7 @@ ERC20_WRAPPER_SOURCE = ''' pragma solidity >=0.7.0; -interface IERC20 { - function decimals() external view returns (uint8); - function totalSupply() external view returns (uint256); - function balanceOf(address who) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); - function transfer(address to, uint256 value) external returns (bool); - function approve(address spender, uint256 value) external returns (bool); - function transferFrom(address from, address to, uint256 value) external returns (bool); - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - function approveSolana(bytes32 spender, uint64 value) external returns (bool); - event ApprovalSolana(address indexed owner, bytes32 indexed spender, uint64 value); -} - -/*abstract*/ contract NeonERC20Wrapper /*is IERC20*/ { +contract NeonERC20Wrapper { address constant NeonERC20 = 0xff00000000000000000000000000000000000001; string public name; @@ -204,15 +191,145 @@ def test_erc20_balanceOf(self): self.assertEqual(b, 0) def test_erc20_transfer(self): + transfer_value = 1000 erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + admin_balance_before = erc20.functions.balanceOf(admin.address).call() + user_balance_before = erc20.functions.balanceOf(user.address).call() + nonce = proxy.eth.get_transaction_count(proxy.eth.default_account) tx = {'nonce': nonce} - tx = erc20.functions.transfer(user.address, 1000).buildTransaction(tx) + tx = erc20.functions.transfer(user.address, transfer_value).buildTransaction(tx) tx = proxy.eth.account.sign_transaction(tx, admin.key) tx_hash = proxy.eth.send_raw_transaction(tx.rawTransaction) - print('tx_hash:',tx_hash) tx_receipt = proxy.eth.wait_for_transaction_receipt(tx_hash) self.assertIsNotNone(tx_receipt) + self.assertEqual(tx_receipt.status, 1) + + admin_balance_after = erc20.functions.balanceOf(admin.address).call() + user_balance_after = erc20.functions.balanceOf(user.address).call() + + self.assertEqual(admin_balance_after, admin_balance_before - transfer_value) + self.assertEqual(user_balance_after, user_balance_before + transfer_value) + + def test_erc20_transfer_not_enough_funds(self): + transfer_value = 100_000_000_000_000 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + admin_balance_before = erc20.functions.balanceOf(admin.address).call() + user_balance_before = erc20.functions.balanceOf(user.address).call() + + with self.assertRaisesRegex(Exception, "ERC20 transfer failed"): + erc20.functions.transfer(user.address, transfer_value).buildTransaction() + + admin_balance_after = erc20.functions.balanceOf(admin.address).call() + user_balance_after = erc20.functions.balanceOf(user.address).call() + + self.assertEqual(admin_balance_after, admin_balance_before) + self.assertEqual(user_balance_after, user_balance_before) + + def test_erc20_transfer_out_of_bounds(self): + transfer_value = 0xFFFF_FFFF_FFFF_FFFF + 1 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + with self.assertRaisesRegex(Exception, "ERC20 transfer failed"): + erc20.functions.transfer(user.address, transfer_value).buildTransaction() + + def test_erc20_approve(self): + approve_value = 1000 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + allowance_before = erc20.functions.allowance(admin.address, user.address).call() + + nonce = proxy.eth.get_transaction_count(admin.address) + tx = erc20.functions.approve(user.address, approve_value).buildTransaction({'nonce': nonce}) + tx = proxy.eth.account.sign_transaction(tx, admin.key) + tx_hash = proxy.eth.send_raw_transaction(tx.rawTransaction) + tx_receipt = proxy.eth.wait_for_transaction_receipt(tx_hash) + self.assertEqual(tx_receipt.status, 1) + + self.assertIsNotNone(tx_receipt) + + allowance_after = erc20.functions.allowance(admin.address, user.address).call() + self.assertEqual(allowance_after, allowance_before + approve_value) + + def test_erc20_transferFrom(self): + approve_value = 1000 + transfer_value = 100 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + nonce = proxy.eth.get_transaction_count(admin.address) + tx = erc20.functions.approve(user.address, approve_value).buildTransaction({'nonce': nonce}) + tx = proxy.eth.account.sign_transaction(tx, admin.key) + tx_hash = proxy.eth.send_raw_transaction(tx.rawTransaction) + tx_receipt = proxy.eth.wait_for_transaction_receipt(tx_hash) + self.assertIsNotNone(tx_receipt) + self.assertEqual(tx_receipt.status, 1) + + allowance_before = erc20.functions.allowance(admin.address, user.address).call() + admin_balance_before = erc20.functions.balanceOf(admin.address).call() + user_balance_before = erc20.functions.balanceOf(user.address).call() + + nonce = proxy.eth.get_transaction_count(user.address) + tx = erc20.functions.transferFrom(admin.address, user.address, transfer_value).buildTransaction( + {'nonce': nonce, 'from': user.address} + ) + tx = proxy.eth.account.sign_transaction(tx, user.key) + tx_hash = proxy.eth.send_raw_transaction(tx.rawTransaction) + tx_receipt = proxy.eth.wait_for_transaction_receipt(tx_hash) + self.assertIsNotNone(tx_receipt) + self.assertEqual(tx_receipt.status, 1) + + allowance_after = erc20.functions.allowance(admin.address, user.address).call() + admin_balance_after = erc20.functions.balanceOf(admin.address).call() + user_balance_after = erc20.functions.balanceOf(user.address).call() + + self.assertEqual(allowance_after, allowance_before - transfer_value) + self.assertEqual(admin_balance_after, admin_balance_before - transfer_value) + self.assertEqual(user_balance_after, user_balance_before + transfer_value) + + def test_erc20_transferFrom_beyond_approve(self): + transfer_value = 10_000_000 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + with self.assertRaisesRegex(Exception, "ERC20 transferFrom failed"): + erc20.functions.transferFrom(admin.address, user.address, transfer_value).buildTransaction( + {'from': user.address} + ) + + def test_erc20_transferFrom_out_of_bounds(self): + transfer_value = 0xFFFF_FFFF_FFFF_FFFF + 1 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + with self.assertRaisesRegex(Exception, "ERC20 transferFrom failed"): + erc20.functions.transferFrom(admin.address, user.address, transfer_value).buildTransaction( + {'from': user.address} + ) + + def test_erc20_approveSolana(self): + delegate = SolanaAccount() + approve_value = 1000 + erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + + nonce = proxy.eth.get_transaction_count(admin.address) + tx = erc20.functions.approveSolana(bytes(delegate.public_key()), approve_value).buildTransaction({'nonce': nonce}) + tx = proxy.eth.account.sign_transaction(tx, admin.key) + tx_hash = proxy.eth.send_raw_transaction(tx.rawTransaction) + tx_receipt = proxy.eth.wait_for_transaction_receipt(tx_hash) + self.assertEqual(tx_receipt.status, 1) + + self.assertIsNotNone(tx_receipt) + + contract_address_bytes = bytes.fromhex(self.contract_address[2:]) + admin_address_bytes = bytes.fromhex(admin.address[2:]) + admin_token_seeds = [ b"\1", b"ERC20Balance", bytes(self.token.pubkey), contract_address_bytes, admin_address_bytes ] + admin_solana_token = PublicKey.find_program_address(admin_token_seeds, evm_loader_id)[0] + + accounts = self.solana_client.get_token_accounts_by_delegate(delegate.public_key(), TokenAccountOpts(mint=self.token.pubkey), commitment=Recent) + accounts = list(map(lambda a: PublicKey(a['pubkey']), accounts['result']['value'])) + + self.assertIn(admin_solana_token, accounts) + if __name__ == '__main__': unittest.main() From 2f4dde6feff03925bbfcd7ce8828609400d81698 Mon Sep 17 00:00:00 2001 From: ivandzen Date: Wed, 22 Dec 2021 21:19:26 +0300 Subject: [PATCH 06/14] =?UTF-8?q?#344=20=D1=81reate=20integration=20tests?= =?UTF-8?q?=20for=20airdropper=20(#370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cherrypick part of changes * create indexer.py * remove solana_receipts_update.py * Cherry pick files from old branch * add requirement * fix refactoring issues * Fix inspection issues * fix last issue * simplify tests * add test * add price provider * fix PriceProvider, add test * Add tests. Check worn on all nets * refactoring * integrate price_provider into airdropper * integrate price provider * use new faucet method * add new parameter to airdropper main * Test discriptions for airdropper * Comments for price provider tests * remove unnecessary comment * fix error * copy from erc20 contract test * create integration tests * fix oneline * revert changes on test_neon_faucet * fix some errors * not working * add old variant * add helper functions * transaction works! * prepare refactoring * remove unnecessary code * add account creation methods * first test almost ready * fix tests * fir airdropper startup * One test is completely ready! * add complex test case * add services to docker-compose file * improve tests * add price update interval parameter * remove unnecessary imports * fix airdropper run * prepare for CI * remove commented * fix compose-file * create wrapper class * make tests work! * fix naming * refactor test_erc20_wrapper_contract * remove duplicated code * Fix tests after merge * improve deployment scripts, get rid of unnecessary delays in integration tests * change wait airdrop logic * use create-test-account.sh script Co-authored-by: ivanl --- .buildkite/steps/deploy-test.sh | 2 + .dockerignore | 3 + proxy/__main__.py | 18 +- proxy/common_neon/erc20_wrapper.py | 200 ++++++++++++++++++ proxy/docker-compose-test.yml | 71 +++++++ proxy/indexer/airdropper.py | 22 +- proxy/indexer/indexer.py | 2 +- proxy/indexer/indexer_base.py | 5 +- proxy/testing/test_airdropper_integration.py | 203 +++++++++++++++++++ proxy/testing/test_erc20_wrapper_contract.py | 141 +++---------- run-airdropper.sh | 10 + run-faucet.sh | 23 +++ run-test-faucet.sh | 30 +++ 13 files changed, 604 insertions(+), 126 deletions(-) create mode 100644 proxy/common_neon/erc20_wrapper.py create mode 100644 proxy/testing/test_airdropper_integration.py create mode 100755 run-airdropper.sh create mode 100755 run-faucet.sh create mode 100755 run-test-faucet.sh diff --git a/.buildkite/steps/deploy-test.sh b/.buildkite/steps/deploy-test.sh index 85ee0f509..298960900 100755 --- a/.buildkite/steps/deploy-test.sh +++ b/.buildkite/steps/deploy-test.sh @@ -43,6 +43,8 @@ function cleanup_docker { if docker logs solana >solana.log 2>&1; then echo "solana logs saved"; fi if docker logs evm_loader >evm_loader.log 2>&1; then echo "evm_loader logs saved"; fi + if docker logs faucet >faucet.log 2>&1; then echo "faucet logs saved"; fi + if docker logs airdropper >airdropper.log 2>&1; then echo "airdropper logs saved"; fi echo "\nCleanup docker-compose..." docker-compose -f proxy/docker-compose-test.yml down --rmi 'all' diff --git a/.dockerignore b/.dockerignore index 32e6c2a0c..3757f215f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,9 @@ !requirements.txt !setup.py !README.md +!run-test-faucet.sh +!run-faucet.sh +!run-airdropper.sh # Ignore __pycache__ directory proxy/__pycache__ diff --git a/proxy/__main__.py b/proxy/__main__.py index 9d9987437..8d21d5200 100644 --- a/proxy/__main__.py +++ b/proxy/__main__.py @@ -12,6 +12,7 @@ from .proxy import entry_point import os from .indexer.airdropper import run_airdropper +from solana.rpc.api import Client if __name__ == '__main__': airdropper_mode = os.environ.get('AIRDROPPER_MODE', 'False').lower() in [1, 'true', 'True'] @@ -20,16 +21,29 @@ solana_url = os.environ['SOLANA_URL'] evm_loader_id = os.environ['EVM_LOADER'] faucet_url = os.environ['FAUCET_URL'] - wrapper_whitelist = os.environ['INDEXER_ERC20_WRAPPER_WHITELIST'].split(',') + wrapper_whitelist = os.environ['INDEXER_ERC20_WRAPPER_WHITELIST'] + if wrapper_whitelist != 'ANY': + wrapper_whitelist = wrapper_whitelist.split(',') log_level = os.environ['LOG_LEVEL'] price_update_interval = int(os.environ.get('PRICE_UPDATE_INTERVAL', '60')) neon_decimals = int(os.environ.get('NEON_DECIMALS', '9')) + + start_slot = os.environ.get('START_SLOT', None) + if start_slot == 'LATEST': + client = Client(solana_url) + start_slot = client.get_slot(commitment="confirmed")["result"] + if start_slot is None: # by default + start_slot = 0 + else: # try to convert into integer + start_slot = int(start_slot) + run_airdropper(solana_url, evm_loader_id, faucet_url, wrapper_whitelist, log_level, price_update_interval, - neon_decimals) + neon_decimals, + start_slot) else: entry_point() diff --git a/proxy/common_neon/erc20_wrapper.py b/proxy/common_neon/erc20_wrapper.py new file mode 100644 index 000000000..6af9fa18d --- /dev/null +++ b/proxy/common_neon/erc20_wrapper.py @@ -0,0 +1,200 @@ +from solcx import install_solc +from web3 import Web3 +from spl.token.client import Token +from spl.token.constants import TOKEN_PROGRAM_ID +from eth_account.signers.local import LocalAccount as NeonAccount +from solana.rpc.api import Account as SolanaAccount +from solana.publickey import PublicKey +from solana.transaction import AccountMeta, TransactionInstruction +from solana.system_program import SYS_PROGRAM_ID +from solana.sysvar import SYSVAR_RENT_PUBKEY +from solana.rpc.types import TxOpts, RPCResponse, Commitment +from solana.transaction import Transaction +import spl.token.instructions as spl_token +from typing import Union, Dict +from logging import getLogger +import struct + +logger = getLogger('__name__') + +install_solc(version='0.7.6') +from solcx import compile_source + +# Standard interface of ERC20 contract to generate ABI for wrapper +ERC20_INTERFACE_SOURCE = ''' +pragma solidity >=0.7.0; + +interface IERC20 { + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address who) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + + function approveSolana(bytes32 spender, uint64 value) external returns (bool); + event ApprovalSolana(address indexed owner, bytes32 indexed spender, uint64 value); +} +''' + +# Copy of contract: https://github.com/neonlabsorg/neon-evm/blob/develop/evm_loader/SPL_ERC20_Wrapper.sol +ERC20_CONTRACT_SOURCE = ''' +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.12; + +/*abstract*/ contract NeonERC20Wrapper /*is IERC20*/ { + address constant NeonERC20 = 0xff00000000000000000000000000000000000001; + + string public name; + string public symbol; + bytes32 public tokenMint; + + constructor( + string memory _name, + string memory _symbol, + bytes32 _tokenMint + ) { + name = _name; + symbol = _symbol; + tokenMint = _tokenMint; + } + + fallback() external { + bytes memory call_data = abi.encodePacked(tokenMint, msg.data); + (bool success, bytes memory result) = NeonERC20.delegatecall(call_data); + + require(success, string(result)); + + assembly { + return(add(result, 0x20), mload(result)) + } + } +} +''' + +class ERC20Wrapper: + proxy: Web3 + name: str + symbol: str + token: Token + admin: NeonAccount + mint_authority: SolanaAccount + evm_loader_id: PublicKey + neon_contract_address: str + solana_contract_address: PublicKey + interface: Dict + wrapper: Dict + + def __init__(self, proxy: Web3, + name: str, symbol: str, + token: Token, + admin: NeonAccount, + mint_authority: SolanaAccount, + evm_loader_id: PublicKey): + self.proxy = proxy + self.name = name + self.symbol = symbol + self.token = token + self.admin = admin + self.mint_authority = mint_authority + self.evm_loader_id = evm_loader_id + + def get_neon_account_address(self, neon_account_address: str) -> PublicKey: + neon_account_addressbytes = bytes.fromhex(neon_account_address[2:]) + return PublicKey.find_program_address([b"\1", neon_account_addressbytes], self.evm_loader_id)[0] + + def deploy_wrapper(self): + compiled_interface = compile_source(ERC20_INTERFACE_SOURCE) + interface_id, interface = compiled_interface.popitem() + self.interface = interface + + compiled_wrapper = compile_source(ERC20_CONTRACT_SOURCE) + wrapper_id, wrapper_interface = compiled_wrapper.popitem() + self.wrapper = wrapper_interface + + erc20 = self.proxy.eth.contract(abi=self.wrapper['abi'], bytecode=wrapper_interface['bin']) + nonce = self.proxy.eth.get_transaction_count(self.proxy.eth.default_account) + tx = {'nonce': nonce} + tx_constructor = erc20.constructor(self.name, self.symbol, bytes(self.token.pubkey)).buildTransaction(tx) + tx_deploy = self.proxy.eth.account.sign_transaction(tx_constructor, self.admin.key) + tx_deploy_hash = self.proxy.eth.send_raw_transaction(tx_deploy.rawTransaction) + logger.debug(f'tx_deploy_hash: {tx_deploy_hash.hex()}') + tx_deploy_receipt = self.proxy.eth.wait_for_transaction_receipt(tx_deploy_hash) + logger.debug(f'tx_deploy_receipt: {tx_deploy_receipt}') + logger.debug(f'deploy status: {tx_deploy_receipt.status}') + self.neon_contract_address = tx_deploy_receipt.contractAddress + self.solana_contract_address = self.get_neon_account_address(self.neon_contract_address) + + def get_neon_erc20_account_address(self, neon_account_address: str): + neon_contract_address_bytes = bytes.fromhex(self.neon_contract_address[2:]) + neon_account_address_bytes = bytes.fromhex(neon_account_address[2:]) + seeds = [b"\1", b"ERC20Balance", + bytes(self.token.pubkey), + neon_contract_address_bytes, + neon_account_address_bytes] + return PublicKey.find_program_address(seeds, self.evm_loader_id)[0] + + def create_associated_token_account(self, owner: PublicKey, payer: SolanaAccount): + # Construct transaction + # This part of code is based on original implementation of Token.create_associated_token_account + # except that skip_preflight is set to True + txn = Transaction() + create_txn = spl_token.create_associated_token_account( + payer=payer.public_key(), owner=owner, mint=self.token.pubkey + ) + txn.add(create_txn) + self.token._conn.send_transaction(txn, payer, opts=TxOpts(skip_preflight = True, skip_confirmation=False)) + return create_txn.keys[1].pubkey + + def create_neon_erc20_account_instruction(self, payer: PublicKey, eth_address: str): + return TransactionInstruction( + program_id=self.evm_loader_id, + data=bytes.fromhex('0F'), + keys=[ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), + AccountMeta(pubkey=self.get_neon_erc20_account_address(eth_address), is_signer=False, is_writable=True), + AccountMeta(pubkey=self.get_neon_account_address(eth_address), is_signer=False, is_writable=True), + AccountMeta(pubkey=self.solana_contract_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=self.token.pubkey, is_signer=False, is_writable=True), + 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), + ] + ) + + def create_input_liquidity_instruction(self, payer: PublicKey, from_address: PublicKey, to_address: str, amount: int): + return TransactionInstruction( + program_id=TOKEN_PROGRAM_ID, + data=b'\3' + struct.pack(' RPCResponse: + """ + Method mints given amount of tokens to a given address - either in NEON or Solana format + NOTE: destination account must be previously created + """ + if isinstance(destination, str): + destination = self.get_neon_erc20_account_address(destination) + return self.token.mint_to(destination, self.mint_authority, amount, + opts=TxOpts(skip_preflight=True, skip_confirmation=False)) + + def erc20_interface(self): + return self.proxy.eth.contract(address=self.neon_contract_address, abi=self.interface['abi']) + + def get_balance(self, address: Union[PublicKey, str]) -> int: + if isinstance(address, PublicKey): + return int(self.token.get_balance(address, Commitment('confirmed'))['result']['value']['amount']) + + erc20 = self.proxy.eth.contract(address=self.neon_contract_address, abi=self.interface['abi']) + return erc20.functions.balanceOf(address).call() diff --git a/proxy/docker-compose-test.yml b/proxy/docker-compose-test.yml index cb142c502..79495f29a 100644 --- a/proxy/docker-compose-test.yml +++ b/proxy/docker-compose-test.yml @@ -82,6 +82,77 @@ services: - net entrypoint: proxy/run-test-proxy.sh + faucet: + container_name: faucet + image: neonlabsorg/proxy:${REVISION} + environment: + FAUCET_RPC_PORT: 3333 + FAUCET_RPC_ALLOWED_ORIGINS: http://airdropper + FAUCET_WEB3_ENABLE: 'false' + WEB3_RPC_URL: http://proxy:9090/solana + WEB3_PRIVATE_KEY: '' + NEON_ERC20_TOKENS: + NEON_ERC20_MAX_AMOUNT: 1000 + FAUCET_SOLANA_ENABLE: 'true' + SOLANA_URL: http://solana:8899 + NEON_OPERATOR_KEYFILE: /root/.config/solana/id.json + NEON_ETH_MAX_AMOUNT: 10 + TEST_FAUCET_INIT_NEON_BALANCE: 10000 + hostname: faucet + expose: + - "3333" + networks: + - net + entrypoint: ./run-test-faucet.sh + depends_on: + proxy: + condition: service_started + + airdropper-postgres: + container_name: airdropper-postgres + image: postgres:14.0 + command: postgres -c 'max_connections=1000' + environment: + POSTGRES_DB: airdropper-db + POSTGRES_USER: neon-airdropper + POSTGRES_PASSWORD: neon-airdropper-pass + hostname: airdropper-postgres + expose: + - "5432" + networks: + - net + healthcheck: + test: [ CMD-SHELL, "pg_isready -h airdropper-postgres -p 5432" ] + interval: 5s + timeout: 10s + retries: 10 + start_period: 5s + + airdropper: + container_name: airdropper + image: neonlabsorg/proxy:${REVISION} + environment: + POSTGRES_DB: airdropper-db + POSTGRES_USER: neon-airdropper + POSTGRES_PASSWORD: neon-airdropper-pass + POSTGRES_HOST: airdropper-postgres + SOLANA_URL: http://solana:8899 + FAUCET_URL: http://faucet:3333 + NEON_CLI_TIMEOUT: 0.9 + INDEXER_ERC20_WRAPPER_WHITELIST: ANY + LOG_LEVEL: INFO + PRICE_UPDATE_INTERVAL: 10 + START_SLOT: LATEST + hostname: airdropper + entrypoint: ./run-airdropper.sh + networks: + - net + depends_on: + airdropper-postgres: + condition: service_healthy + faucet: + condition: service_started + networks: net: diff --git a/proxy/indexer/airdropper.py b/proxy/indexer/airdropper.py index 96472da10..ab378b7e6 100644 --- a/proxy/indexer/airdropper.py +++ b/proxy/indexer/airdropper.py @@ -22,11 +22,12 @@ def __init__(self, solana_url, evm_loader_id, faucet_url = '', - wrapper_whitelist = [], + wrapper_whitelist = 'ANY', log_level = 'INFO', price_upd_interval=60, - neon_decimals = 9): - IndexerBase.__init__(self, solana_url, evm_loader_id, log_level) + neon_decimals = 9, + start_slot = 0): + IndexerBase.__init__(self, solana_url, evm_loader_id, log_level, start_slot) # collection of eth-address-to-create-accout-trx mappings # for every addresses that was already funded with airdrop @@ -37,13 +38,15 @@ def __init__(self, # Price provider need pyth.network be deployed onto solana # so using mainnet solana for simplicity self.price_provider = PriceProvider(mainnet_solana, - price_upd_interval, + price_upd_interval, # seconds mainnet_price_accounts) self.neon_decimals = neon_decimals # helper function checking if given contract address is in whitelist def _is_allowed_wrapper_contract(self, contract_addr): + if self.wrapper_whitelist == 'ANY': + return True return contract_addr in self.wrapper_whitelist @@ -159,10 +162,11 @@ def process_receipts(self): def run_airdropper(solana_url, evm_loader_id, faucet_url = '', - wrapper_whitelist = [], + wrapper_whitelist = 'ANY', log_level = 'INFO', price_update_interval = 60, - neon_decimals = 9): + neon_decimals = 9, + start_slot = 0): logging.basicConfig(format='%(asctime)s - pid:%(process)d [%(levelname)-.1s] %(funcName)s:%(lineno)d - %(message)s') logger.setLevel(logging.DEBUG) logger.info(f"""Running indexer with params: @@ -172,7 +176,8 @@ def run_airdropper(solana_url, faucet_url: {faucet_url}, wrapper_whitelist: {wrapper_whitelist}, price update interval: {price_update_interval}, - NEON decimals: {neon_decimals}""") + NEON decimals: {neon_decimals}, + Start slot: {start_slot}""") airdropper = Airdropper(solana_url, evm_loader_id, @@ -180,5 +185,6 @@ def run_airdropper(solana_url, wrapper_whitelist, log_level, price_update_interval, - neon_decimals) + neon_decimals, + start_slot) airdropper.run() diff --git a/proxy/indexer/indexer.py b/proxy/indexer/indexer.py index 9c1e4c9fd..a5c3e7185 100644 --- a/proxy/indexer/indexer.py +++ b/proxy/indexer/indexer.py @@ -53,7 +53,7 @@ def __init__(self, solana_url, evm_loader_id, log_level = 'INFO'): - IndexerBase.__init__(self, solana_url, evm_loader_id, log_level) + IndexerBase.__init__(self, solana_url, evm_loader_id, log_level, 0) self.canceller = Canceller() self.logs_db = LogDB() diff --git a/proxy/indexer/indexer_base.py b/proxy/indexer/indexer_base.py index cda952bbf..33e34616d 100644 --- a/proxy/indexer/indexer_base.py +++ b/proxy/indexer/indexer_base.py @@ -34,13 +34,14 @@ class IndexerBase: def __init__(self, solana_url, evm_loader_id, - log_level): + log_level, + start_slot): logger.setLevel(log_levels.get(log_level, logging.INFO)) self.evm_loader_id = evm_loader_id self.client = Client(solana_url) self.transaction_receipts = SQLDict(tablename="known_transactions") - self.last_slot = 0 + self.last_slot = start_slot self.current_slot = 0 self.transaction_order = [] self.counter_ = 0 diff --git a/proxy/testing/test_airdropper_integration.py b/proxy/testing/test_airdropper_integration.py new file mode 100644 index 000000000..ff9464832 --- /dev/null +++ b/proxy/testing/test_airdropper_integration.py @@ -0,0 +1,203 @@ +from unittest import TestCase + +from solana.rpc.api import Client as SolanaClient +from solana.account import Account as SolanaAccount +from spl.token.client import Token as SplToken +from spl.token.instructions import get_associated_token_address +from proxy.environment import SOLANA_URL, EVM_LOADER_ID, ETH_TOKEN_MINT_ID +from solana.system_program import SYS_PROGRAM_ID +from solana.sysvar import SYSVAR_RENT_PUBKEY +from spl.token.constants import TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID +from solana.rpc.commitment import Confirmed +from solana.publickey import PublicKey +from solana.rpc.types import TxOpts +from solana.transaction import TransactionInstruction, Transaction, AccountMeta +from proxy.common_neon.neon_instruction import create_account_layout +from proxy.common_neon.erc20_wrapper import ERC20Wrapper +from time import sleep +from web3 import Web3 +import os +import json + +MAX_AIRDROP_WAIT_TIME = 45 +EVM_LOADER_ID = PublicKey(EVM_LOADER_ID) +PROXY_URL = os.environ.get('PROXY_URL', 'http://localhost:9090/solana') +FAUCET_RPC_PORT = 3333 +NAME = 'TestToken' +SYMBOL = 'TST' +proxy = Web3(Web3.HTTPProvider(PROXY_URL)) +admin = proxy.eth.account.create('neonlabsorg/proxy-model.py/issues/344/admin20') +proxy.eth.default_account = admin.address + + +# Helper function calculating solana address and nonce from given NEON(Ethereum) address +def get_evm_loader_account_address(eth_address: str): + eth_addressbytes = bytes.fromhex(eth_address[2:]) + return PublicKey.find_program_address([b"\1", eth_addressbytes], EVM_LOADER_ID) + + +class TestAirdropperIntegration(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.create_token_mint(cls) + cls.deploy_erc20_wrapper_contract(cls) + cls.acc_num = 0 + + def create_token_mint(self): + self.solana_client = SolanaClient(SOLANA_URL) + + with open("proxy/operator-keypair.json") as f: + d = json.load(f) + self.mint_authority = SolanaAccount(d[0:32]) + self.solana_client.request_airdrop(self.mint_authority.public_key(), 1000_000_000_000, Confirmed) + + while True: + balance = self.solana_client.get_balance(self.mint_authority.public_key(), Confirmed)["result"]["value"] + if balance > 0: + break + sleep(1) + print('create_token_mint mint, SolanaAccount: ', self.mint_authority.public_key()) + + self.token = SplToken.create_mint( + self.solana_client, + self.mint_authority, + self.mint_authority.public_key(), + 9, + TOKEN_PROGRAM_ID, + ) + + def deploy_erc20_wrapper_contract(self): + self.wrapper = ERC20Wrapper(proxy, NAME, SYMBOL, + self.token, admin, + self.mint_authority, + EVM_LOADER_ID) + self.wrapper.deploy_wrapper() + + def create_account_instruction(self, eth_address: str, payer: PublicKey): + dest_address_solana, nonce = get_evm_loader_account_address(eth_address) + neon_token_account = get_associated_token_address(dest_address_solana, ETH_TOKEN_MINT_ID) + return TransactionInstruction( + program_id=EVM_LOADER_ID, + data=create_account_layout(0, 0, bytes.fromhex(eth_address[2:]), nonce), + keys=[ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), + AccountMeta(pubkey=dest_address_solana, is_signer=False, is_writable=True), + AccountMeta(pubkey=neon_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=ETH_TOKEN_MINT_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=ASSOCIATED_TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), + ]) + + def create_sol_account(self): + account = SolanaAccount() + print(f"New solana account created: {account.public_key().to_base58()}. Airdropping...") + self.solana_client.request_airdrop(account.public_key(), 1000_000_000_000, Confirmed) + return account + + def create_token_account(self, owner: PublicKey, mint_amount: int): + new_token_account = self.wrapper.create_associated_token_account(owner, self.mint_authority) + self.wrapper.mint_to(new_token_account, mint_amount) + return new_token_account + + def create_eth_account(self): + self.acc_num += 1 + account = proxy.eth.account.create(f'neonlabsorg/proxy-model.py/issues/344/eth_account{self.acc_num}') + print(f"NEON account created: {account.address}") + return account + + def test_success_airdrop_simple_case(self): + from_owner = self.create_sol_account() + mint_amount = 1000_000_000_000 + from_spl_token_acc = self.create_token_account(from_owner.public_key(), mint_amount) + to_neon_acc = self.create_eth_account().address + + self.assertEqual(self.wrapper.get_balance(from_spl_token_acc), mint_amount) + self.assertEqual(self.wrapper.get_balance(to_neon_acc), 0) + + TRANSFER_AMOUNT = 123456 + trx = Transaction() + trx.add(self.create_account_instruction(to_neon_acc, from_owner.public_key())) + trx.add(self.wrapper.create_neon_erc20_account_instruction(from_owner.public_key(), to_neon_acc)) + trx.add(self.wrapper.create_input_liquidity_instruction(from_owner.public_key(), + from_spl_token_acc, + to_neon_acc, + TRANSFER_AMOUNT)) + + opts = TxOpts(skip_preflight=True, skip_confirmation=False) + print(self.solana_client.send_transaction(trx, from_owner, opts=opts)) + + self.assertEqual(self.wrapper.get_balance(from_spl_token_acc), mint_amount - TRANSFER_AMOUNT) + self.assertEqual(self.wrapper.get_balance(to_neon_acc), TRANSFER_AMOUNT) + + wait_time = 0 + eth_balance = 0 + while wait_time < MAX_AIRDROP_WAIT_TIME: + eth_balance = proxy.eth.get_balance(to_neon_acc) + balance_ready = eth_balance > 0 and eth_balance < 10 * pow(10, 18) + if balance_ready: + break + sleep(1) + wait_time += 1 + print(f"Wait time for simple transaction (1 airdrop): {wait_time}") + + eth_balance = proxy.eth.get_balance(to_neon_acc) + print("NEON balance is: ", eth_balance) + self.assertTrue(eth_balance > 0 and eth_balance < 10 * pow(10, 18)) # 10 NEON is a max airdrop amount + + def test_success_airdrop_complex_case(self): + from_owner = self.create_sol_account() + mint_amount = 1000_000_000_000 + from_spl_token_acc = self.create_token_account(from_owner.public_key(), mint_amount) + to_neon_acc1 = self.create_eth_account().address + to_neon_acc2 = self.create_eth_account().address + + self.assertEqual(self.wrapper.get_balance(from_spl_token_acc), mint_amount) + self.assertEqual(self.wrapper.get_balance(to_neon_acc1), 0) + self.assertEqual(self.wrapper.get_balance(to_neon_acc2), 0) + + TRANSFER_AMOUNT1 = 123456 + TRANSFER_AMOUNT2 = 654321 + trx = Transaction() + trx.add(self.create_account_instruction(to_neon_acc1, from_owner.public_key())) + trx.add(self.create_account_instruction(to_neon_acc2, from_owner.public_key())) + trx.add(self.wrapper.create_neon_erc20_account_instruction(from_owner.public_key(), to_neon_acc1)) + trx.add(self.wrapper.create_neon_erc20_account_instruction(from_owner.public_key(), to_neon_acc2)) + trx.add(self.wrapper.create_input_liquidity_instruction(from_owner.public_key(), + from_spl_token_acc, + to_neon_acc1, + TRANSFER_AMOUNT1)) + trx.add(self.wrapper.create_input_liquidity_instruction(from_owner.public_key(), + from_spl_token_acc, + to_neon_acc2, + TRANSFER_AMOUNT2)) + + opts = TxOpts(skip_preflight=True, skip_confirmation=False) + print(self.solana_client.send_transaction(trx, from_owner, opts=opts)) + + self.assertEqual(self.wrapper.get_balance(from_spl_token_acc), mint_amount - TRANSFER_AMOUNT1 - TRANSFER_AMOUNT2) + self.assertEqual(self.wrapper.get_balance(to_neon_acc1), TRANSFER_AMOUNT1) + self.assertEqual(self.wrapper.get_balance(to_neon_acc2), TRANSFER_AMOUNT2) + + + wait_time = 0 + eth_balance1 = 0 + eth_balance2 = 0 + while wait_time < MAX_AIRDROP_WAIT_TIME: + eth_balance1 = proxy.eth.get_balance(to_neon_acc1) + eth_balance2 = proxy.eth.get_balance(to_neon_acc2) + balance1_ready = eth_balance1 > 0 and eth_balance1 < 10 * pow(10, 18) + balance2_ready = eth_balance2 > 0 and eth_balance2 < 10 * pow(10, 18) + if balance1_ready and balance2_ready: + break + sleep(1) + wait_time += 1 + print(f"Wait time for complex transaction (2 airdrops): {wait_time}") + + eth_balance1 = proxy.eth.get_balance(to_neon_acc1) + eth_balance2 = proxy.eth.get_balance(to_neon_acc2) + print("NEON balance 1 is: ", eth_balance1) + print("NEON balance 2 is: ", eth_balance2) + self.assertTrue(eth_balance1 > 0 and eth_balance1 < 10 * pow(10, 18)) # 10 NEON is a max airdrop amount + self.assertTrue(eth_balance2 > 0 and eth_balance2 < 10 * pow(10, 18)) # 10 NEON is a max airdrop amount diff --git a/proxy/testing/test_erc20_wrapper_contract.py b/proxy/testing/test_erc20_wrapper_contract.py index b25f0fdb0..e8f04abbf 100644 --- a/proxy/testing/test_erc20_wrapper_contract.py +++ b/proxy/testing/test_erc20_wrapper_contract.py @@ -8,24 +8,18 @@ from solana.rpc.commitment import Commitment, Confirmed, Recent from solana.rpc.types import TxOpts from web3 import Web3 -from solcx import install_solc from spl.token.client import Token as SplToken from spl.token.constants import TOKEN_PROGRAM_ID from solana.rpc.api import Client as SolanaClient from solana.account import Account as SolanaAccount from solana.publickey import PublicKey -from solana.rpc.types import TokenAccountOpts - +from proxy.environment import EVM_LOADER_ID +from proxy.common_neon.erc20_wrapper import ERC20Wrapper from proxy.common_neon.neon_instruction import NeonInstruction +from solana.rpc.types import TokenAccountOpts -# install_solc(version='latest') -install_solc(version='0.7.6') -from solcx import compile_source - -EXTRA_GAS = int(os.environ.get("EXTRA_GAS", "100000")) proxy_url = os.environ.get('PROXY_URL', 'http://127.0.0.1:9090/solana') solana_url = os.environ.get("SOLANA_URL", "http://127.0.0.1:8899") -evm_loader_id = PublicKey(os.environ.get("EVM_LOADER")) proxy = Web3(Web3.HTTPProvider(proxy_url)) admin = proxy.eth.account.create('issues/neonlabsorg/proxy-model.py/197/admin') user = proxy.eth.account.create('issues/neonlabsorg/proxy-model.py/197/user') @@ -34,60 +28,6 @@ NAME = 'NEON' SYMBOL = 'NEO' -# Standard interface of ERC20 contract to generate ABI for wrapper -ERC20_INTERFACE_SOURCE = ''' -pragma solidity >=0.7.0; - -interface IERC20 { - function decimals() external view returns (uint8); - function totalSupply() external view returns (uint256); - function balanceOf(address who) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); - function transfer(address to, uint256 value) external returns (bool); - function approve(address spender, uint256 value) external returns (bool); - function transferFrom(address from, address to, uint256 value) external returns (bool); - function approveSolana(bytes32 spender, uint64 value) external returns (bool); - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - event ApprovalSolana(address indexed owner, bytes32 indexed spender, uint64 value); -} -''' - -# Copy of contract: https://github.com/neonlabsorg/neon-evm/blob/develop/evm_loader/SPL_ERC20_Wrapper.sol -ERC20_WRAPPER_SOURCE = ''' -pragma solidity >=0.7.0; - -contract NeonERC20Wrapper { - address constant NeonERC20 = 0xff00000000000000000000000000000000000001; - - string public name; - string public symbol; - bytes32 public tokenMint; - - constructor( - string memory _name, - string memory _symbol, - bytes32 _tokenMint - ) { - name = _name; - symbol = _symbol; - tokenMint = _tokenMint; - } - - fallback() external { - bytes memory call_data = abi.encodePacked(tokenMint, msg.data); - (bool success, bytes memory result) = NeonERC20.delegatecall(call_data); - - require(success, string(result)); - - assembly { - return(add(result, 0x20), mload(result)) - } - } -} -''' - class Test_erc20_wrapper_contract(unittest.TestCase): @classmethod def setUpClass(cls): @@ -105,7 +45,6 @@ def setUpClass(cls): def create_token_mint(self): self.solana_client = SolanaClient(solana_url) - # with open("/root/.config/solana/id.json") as f: with open("proxy/operator-keypair.json") as f: d = json.load(f) self.solana_account = SolanaAccount(d[0:32]) @@ -127,64 +66,46 @@ def create_token_mint(self): ) def deploy_erc20_wrapper_contract(self): - compiled_interface = compile_source(ERC20_INTERFACE_SOURCE) - interface_id, interface = compiled_interface.popitem() - self.interface = interface - - compiled_wrapper = compile_source(ERC20_WRAPPER_SOURCE) - wrapper_id, wrapper_interface = compiled_wrapper.popitem() - self.wrapper = wrapper_interface - - erc20 = proxy.eth.contract(abi=self.wrapper['abi'], bytecode=wrapper_interface['bin']) - nonce = proxy.eth.get_transaction_count(proxy.eth.default_account) - tx = {'nonce': nonce} - tx_constructor = erc20.constructor(NAME, SYMBOL, bytes(self.token.pubkey)).buildTransaction(tx) - tx_deploy = proxy.eth.account.sign_transaction(tx_constructor, admin.key) - #print('tx_deploy:', tx_deploy) - tx_deploy_hash = proxy.eth.send_raw_transaction(tx_deploy.rawTransaction) - print('tx_deploy_hash:', tx_deploy_hash.hex()) - tx_deploy_receipt = proxy.eth.wait_for_transaction_receipt(tx_deploy_hash) - print('tx_deploy_receipt:', tx_deploy_receipt) - print('deploy status:', tx_deploy_receipt.status) - self.contract_address = tx_deploy_receipt.contractAddress + self.wrapper = ERC20Wrapper(proxy, NAME, SYMBOL, + self.token, admin, + self.solana_account, + PublicKey(EVM_LOADER_ID)) + self.wrapper.deploy_wrapper() def create_token_accounts(self): - contract_address_bytes = bytes.fromhex(self.contract_address[2:]) - contract_address_solana = PublicKey.find_program_address([b"\1", contract_address_bytes], evm_loader_id)[0] + admin_token_key = self.wrapper.get_neon_erc20_account_address(admin.address) - admin_address_bytes = bytes.fromhex(admin.address[2:]) - admin_address_solana = PublicKey.find_program_address([b"\1", admin_address_bytes], evm_loader_id)[0] - - admin_token_seeds = [ b"\1", b"ERC20Balance", bytes(self.token.pubkey), contract_address_bytes, admin_address_bytes ] - admin_token_key = PublicKey.find_program_address(admin_token_seeds, evm_loader_id)[0] - admin_token_info = { "key": admin_token_key, "owner": admin_address_solana, "contract": contract_address_solana, "mint": self.token.pubkey } + admin_token_info = { "key": admin_token_key, + "owner": self.wrapper.get_neon_account_address(admin.address), + "contract": self.wrapper.solana_contract_address, + "mint": self.token.pubkey } instr = NeonInstruction(self.solana_account.public_key()).createERC20TokenAccountTrx(admin_token_info) self.solana_client.send_transaction(instr, self.solana_account, opts=TxOpts(skip_preflight=True, skip_confirmation=False)) - self.token.mint_to(admin_token_key, self.solana_account, 10_000_000_000_000, opts=TxOpts(skip_preflight=True, skip_confirmation=False)) + self.wrapper.mint_to(admin_token_key, 10_000_000_000_000) def test_erc20_name(self): - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.wrapper['abi']) + erc20 = proxy.eth.contract(address=self.wrapper.neon_contract_address, abi=self.wrapper.wrapper['abi']) name = erc20.functions.name().call() self.assertEqual(name, NAME) def test_erc20_symbol(self): - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.wrapper['abi']) + erc20 = proxy.eth.contract(address=self.wrapper.neon_contract_address, abi=self.wrapper.wrapper['abi']) sym = erc20.functions.symbol().call() self.assertEqual(sym, SYMBOL) def test_erc20_decimals(self): - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() decs = erc20.functions.decimals().call() self.assertEqual(decs, 9) def test_erc20_totalSupply(self): - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() ts = erc20.functions.totalSupply().call() self.assertGreater(ts, 0) def test_erc20_balanceOf(self): - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() b = erc20.functions.balanceOf(admin.address).call() self.assertGreater(b, 0) b = erc20.functions.balanceOf(user.address).call() @@ -192,7 +113,7 @@ def test_erc20_balanceOf(self): def test_erc20_transfer(self): transfer_value = 1000 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() admin_balance_before = erc20.functions.balanceOf(admin.address).call() user_balance_before = erc20.functions.balanceOf(user.address).call() @@ -214,7 +135,7 @@ def test_erc20_transfer(self): def test_erc20_transfer_not_enough_funds(self): transfer_value = 100_000_000_000_000 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() admin_balance_before = erc20.functions.balanceOf(admin.address).call() user_balance_before = erc20.functions.balanceOf(user.address).call() @@ -230,14 +151,14 @@ def test_erc20_transfer_not_enough_funds(self): def test_erc20_transfer_out_of_bounds(self): transfer_value = 0xFFFF_FFFF_FFFF_FFFF + 1 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() with self.assertRaisesRegex(Exception, "ERC20 transfer failed"): erc20.functions.transfer(user.address, transfer_value).buildTransaction() def test_erc20_approve(self): approve_value = 1000 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() allowance_before = erc20.functions.allowance(admin.address, user.address).call() @@ -256,7 +177,7 @@ def test_erc20_approve(self): def test_erc20_transferFrom(self): approve_value = 1000 transfer_value = 100 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() nonce = proxy.eth.get_transaction_count(admin.address) tx = erc20.functions.approve(user.address, approve_value).buildTransaction({'nonce': nonce}) @@ -290,7 +211,7 @@ def test_erc20_transferFrom(self): def test_erc20_transferFrom_beyond_approve(self): transfer_value = 10_000_000 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() with self.assertRaisesRegex(Exception, "ERC20 transferFrom failed"): erc20.functions.transferFrom(admin.address, user.address, transfer_value).buildTransaction( @@ -299,7 +220,7 @@ def test_erc20_transferFrom_beyond_approve(self): def test_erc20_transferFrom_out_of_bounds(self): transfer_value = 0xFFFF_FFFF_FFFF_FFFF + 1 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() with self.assertRaisesRegex(Exception, "ERC20 transferFrom failed"): erc20.functions.transferFrom(admin.address, user.address, transfer_value).buildTransaction( @@ -309,7 +230,7 @@ def test_erc20_transferFrom_out_of_bounds(self): def test_erc20_approveSolana(self): delegate = SolanaAccount() approve_value = 1000 - erc20 = proxy.eth.contract(address=self.contract_address, abi=self.interface['abi']) + erc20 = self.wrapper.erc20_interface() nonce = proxy.eth.get_transaction_count(admin.address) tx = erc20.functions.approveSolana(bytes(delegate.public_key()), approve_value).buildTransaction({'nonce': nonce}) @@ -319,16 +240,10 @@ def test_erc20_approveSolana(self): self.assertEqual(tx_receipt.status, 1) self.assertIsNotNone(tx_receipt) - - contract_address_bytes = bytes.fromhex(self.contract_address[2:]) - admin_address_bytes = bytes.fromhex(admin.address[2:]) - admin_token_seeds = [ b"\1", b"ERC20Balance", bytes(self.token.pubkey), contract_address_bytes, admin_address_bytes ] - admin_solana_token = PublicKey.find_program_address(admin_token_seeds, evm_loader_id)[0] - accounts = self.solana_client.get_token_accounts_by_delegate(delegate.public_key(), TokenAccountOpts(mint=self.token.pubkey), commitment=Recent) accounts = list(map(lambda a: PublicKey(a['pubkey']), accounts['result']['value'])) - self.assertIn(admin_solana_token, accounts) + self.assertIn(self.wrapper.get_neon_erc20_account_address(admin.address), accounts) if __name__ == '__main__': diff --git a/run-airdropper.sh b/run-airdropper.sh new file mode 100755 index 000000000..7bded6369 --- /dev/null +++ b/run-airdropper.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -z "$EVM_LOADER" ]; then + echo "Extracting EVM_LOADER address from keypair file..." + export EVM_LOADER=$(solana address -k /spl/bin/evm_loader-keypair.json) + echo "EVM_LOADER=$EVM_LOADER" +fi +export AIRDROPPER_MODE='true' + +python3 -m proxy diff --git a/run-faucet.sh b/run-faucet.sh new file mode 100755 index 000000000..7db59c021 --- /dev/null +++ b/run-faucet.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +if [ -z "$SOLANA_URL" ]; then + echo "SOLANA_URL is not set" + exit 1 +fi + +echo "Extracting NEON-EVM's ELF parameters" +export EVM_LOADER=$(solana address -k /spl/bin/evm_loader-keypair.json) +export $(/spl/bin/neon-cli --commitment confirmed --url $SOLANA_URL --evm_loader="$EVM_LOADER" neon-elf-params) + +BALANCE=$(solana balance | tr '.' '\t'| tr '[:space:]' '\t' | cut -f1) +if [ "$BALANCE" -eq 0 ]; then + echo "SOL balance is 0" + exit 1 +fi + +if [ "$(spl-token balance "$NEON_TOKEN_MINT" || echo 0)" -eq 0 ]; then + echo "NEON balance is 0" + exit 1 +fi + +faucet run --workers 1 \ No newline at end of file diff --git a/run-test-faucet.sh b/run-test-faucet.sh new file mode 100755 index 000000000..858427729 --- /dev/null +++ b/run-test-faucet.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +if [ -z "$SOLANA_URL" ]; then + echo "SOLANA_URL is not set" + exit 1 +fi + +if [ -z "$TEST_FAUCET_INIT_NEON_BALANCE" ]; then + echo "TEST_FAUCET_INIT_NEON_BALANCE is not set" + exit 1 +fi + +solana config set -u "$SOLANA_URL" + +echo "Extracting NEON-EVM's ELF parameters" +export EVM_LOADER=$(solana address -k /spl/bin/evm_loader-keypair.json) +export $(/spl/bin/neon-cli --commitment confirmed --url $SOLANA_URL --evm_loader="$EVM_LOADER" neon-elf-params) + +echo "Generating new account for operate with faucet service" +rm /$HOME/.config/solana/id.json +/spl/bin/create-test-accounts.sh 1 + +if [ "$(spl-token balance "$NEON_TOKEN_MINT" || echo 0)" -eq 0 ]; then + echo 'Create balance and mint token' + TOKEN_ACCOUNT=$( (spl-token create-account "$NEON_TOKEN_MINT" || true) | grep -Po 'Creating account \K[^\n]*') + echo "TOKEN_ACCOUNT=$TOKEN_ACCOUNT" + spl-token mint "$NEON_TOKEN_MINT" $TEST_FAUCET_INIT_NEON_BALANCE --owner /spl/bin/evm_loader-keypair.json -- "$TOKEN_ACCOUNT" +fi + +./run-faucet.sh From 8dc99fd087fbdd63f72167c4219e6f1cf0248422 Mon Sep 17 00:00:00 2001 From: ivandzen Date: Thu, 23 Dec 2021 16:01:10 +0300 Subject: [PATCH 07/14] #407 Don't create neon account if NEW_USER_AIRDROP_AMOUNT is zero (#408) Co-authored-by: ivanl --- proxy/plugin/solana_rest_api_tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proxy/plugin/solana_rest_api_tools.py b/proxy/plugin/solana_rest_api_tools.py index 73085b405..ed4a71ff9 100644 --- a/proxy/plugin/solana_rest_api_tools.py +++ b/proxy/plugin/solana_rest_api_tools.py @@ -15,7 +15,7 @@ from ..common_neon.transaction_sender import TransactionSender from ..common_neon.emulator_interactor import call_emulated from ..common_neon.utils import get_from_dict -from ..environment import read_elf_params, TIMEOUT_TO_RELOAD_NEON_CONFIG, EXTRA_GAS +from ..environment import NEW_USER_AIRDROP_AMOUNT, read_elf_params, TIMEOUT_TO_RELOAD_NEON_CONFIG, EXTRA_GAS logger = logging.getLogger(__name__) @@ -104,6 +104,9 @@ def get_token_balance_or_airdrop(client: SolanaClient, signer: SolanaAccount, et return get_token_balance_gwei(client, solana_account) except SolanaAccountNotFoundError: logger.debug(f"Account not found: {eth_account} aka: {solana_account} - create") + if NEW_USER_AIRDROP_AMOUNT == 0: + return 0 + create_eth_account_and_airdrop(client, signer, eth_account) return get_token_balance_gwei(client, solana_account) From 8e73562c8f3c58f1ac2a1eeaed20bb1f4a2a54cc Mon Sep 17 00:00:00 2001 From: sinev-valentine <37595780+sinev-valentine@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:22:55 +0300 Subject: [PATCH 08/14] #382 implement the calculation create_program_address by python (#383) * impl * fix last commit * fix seed list --- proxy/common_neon/address.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/proxy/common_neon/address.py b/proxy/common_neon/address.py index 462ca8564..842d518a4 100644 --- a/proxy/common_neon/address.py +++ b/proxy/common_neon/address.py @@ -9,7 +9,7 @@ from .layouts import ACCOUNT_INFO_LAYOUT from ..environment import neon_cli, ETH_TOKEN_MINT_ID, EVM_LOADER_ID - +from .constants import ACCOUNT_SEED_VERSION logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -50,9 +50,12 @@ def ether2program(ether): ether = str(ether) else: ether = ether.hex() - output = neon_cli().call("create-program-address", ether) - items = output.rstrip().split(' ') - return items[0], int(items[1]) + + if ether[0:2] == '0x': + ether = ether[2:] + seed = [ACCOUNT_SEED_VERSION, bytes.fromhex(ether)] + (pda, nonce) = PublicKey.find_program_address(seed, PublicKey(EVM_LOADER_ID)) + return str(pda), nonce def getTokenAddr(account): From 5a5b916a0ddc5973752caabf7d2f3a6b7265924b Mon Sep 17 00:00:00 2001 From: sinev-valentine <37595780+sinev-valentine@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:40:13 +0300 Subject: [PATCH 09/14] #393 add primary key to OPERATOR_COST table (#394) --- proxy/common_neon/costs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/common_neon/costs.py b/proxy/common_neon/costs.py index 86e58dd7e..8b9be0127 100644 --- a/proxy/common_neon/costs.py +++ b/proxy/common_neon/costs.py @@ -19,6 +19,7 @@ def __init__(self): cur.execute(''' CREATE TABLE IF NOT EXISTS OPERATOR_COST ( + id SERIAL PRIMARY KEY, hash char(64), cost bigint, used_gas bigint, From 20718b7e5032da1bb7b9a43d50ec5b132772be41 Mon Sep 17 00:00:00 2001 From: sinev-valentine <37595780+sinev-valentine@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:44:41 +0300 Subject: [PATCH 10/14] #230 fix problem with transfer eth to himself (#381) --- proxy/common_neon/transaction_sender.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proxy/common_neon/transaction_sender.py b/proxy/common_neon/transaction_sender.py index 8831aedb6..c0f53bcc4 100644 --- a/proxy/common_neon/transaction_sender.py +++ b/proxy/common_neon/transaction_sender.py @@ -271,9 +271,10 @@ def create_account_list_by_emulate(self): code_sol = None code_writable = None - elif address == sender_ether: + if address == sender_ether: sender_sol = PublicKey(acc_desc["account"]) - else: + + if address != to_address and address != sender_ether: add_keys_05.append(AccountMeta(pubkey=acc_desc["account"], is_signer=False, is_writable=True)) if acc_desc["new"]: if code_account: From eb2f888c244b3c303a06dcbd7d41fa79af927d42 Mon Sep 17 00:00:00 2001 From: sinev-valentine <37595780+sinev-valentine@users.noreply.github.com> Date: Fri, 24 Dec 2021 19:10:02 +0300 Subject: [PATCH 11/14] #411 Added the argument "--token_mint" to emulate command (#404) * impl * fix emulate call * set temporary evm_loader branch * fix last commit * temporary commit: set evm_loader version * fix emulate * Revert "temporary commit: set evm_loader version" This reverts commit 23832122fddd4cb2a5f22854fa09cebb77bade75. * Revert "fix last commit" This reverts commit c51e198f00c2d3b19fd28cceef37ecc400dae3b3. * Revert "set temporary evm_loader branch" This reverts commit 5bb8c772c0a1919a141daf7def2da8006fbec506. --- proxy/common_neon/emulator_interactor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/common_neon/emulator_interactor.py b/proxy/common_neon/emulator_interactor.py index b8d4720d6..869c12036 100644 --- a/proxy/common_neon/emulator_interactor.py +++ b/proxy/common_neon/emulator_interactor.py @@ -3,7 +3,7 @@ from typing import Optional, Dict, Any from .errors import EthereumError -from ..environment import neon_cli +from ..environment import neon_cli, ETH_TOKEN_MINT_ID logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -63,4 +63,4 @@ def decode_revert_message(data: str) -> Optional[str]: def emulator(contract, sender, data, value): data = data or "none" value = value or "" - return neon_cli().call("emulate", sender, contract, data, value) + return neon_cli().call("emulate", "--token_mint", str(ETH_TOKEN_MINT_ID), sender, contract, data, value) From 67e5656022ac49182946ecccb5b4e4e1347783c4 Mon Sep 17 00:00:00 2001 From: Anton Lisanin Date: Mon, 27 Dec 2021 15:26:27 +0000 Subject: [PATCH 12/14] R#406 Reduce the number of requests to solana rpc (#405) * Reduce the number of requests to solana rpc * Use JSON RPC batch processing * Do not send create accounts trx if it was already sent * Use batch processing for rent exempt balance * Match between the set of Request objects and the resulting set of Response --- proxy/common_neon/solana_interactor.py | 134 +++++++++++++++++++----- proxy/common_neon/transaction_sender.py | 77 +++++--------- 2 files changed, 136 insertions(+), 75 deletions(-) diff --git a/proxy/common_neon/solana_interactor.py b/proxy/common_neon/solana_interactor.py index 10fc20633..c5017be78 100644 --- a/proxy/common_neon/solana_interactor.py +++ b/proxy/common_neon/solana_interactor.py @@ -4,33 +4,61 @@ import logging import re import time +import requests from solana.blockhash import Blockhash +from solana.publickey import PublicKey from solana.rpc.api import Client as SolanaClient from solana.rpc.api import SendTransactionError from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts +from solana.rpc.types import RPCResponse, TxOpts from solana.transaction import Transaction +from urllib.parse import urlparse +from itertools import zip_longest from .costs import update_transaction_cost from .utils import get_from_dict from ..environment import EVM_LOADER_ID, CONFIRMATION_CHECK_DELAY, LOG_SENDING_SOLANA_TRANSACTION, RETRY_ON_FAIL +from typing import Any, List, NamedTuple, Union, cast + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +class AccountInfo(NamedTuple): + tag: int + lamports: int + owner: PublicKey class SolanaInteractor: def __init__(self, signer, client: SolanaClient) -> None: self.signer = signer self.client = client + def _send_rpc_batch_request(self, method: str, params_list: List[Any]) -> List[RPCResponse]: + request_data = [] + for params in params_list: + request_id = next(self.client._provider._request_counter) + 1 + request = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params} + request_data.append(request) + + response = requests.post(self.client._provider.endpoint_uri, headers={"Content-Type": "application/json"}, json=request_data) + response.raise_for_status() + + response_data = cast(List[RPCResponse], response.json()) + response_data.sort(key=lambda r: r["id"]) + + for request, response in zip_longest(request_data, response_data): + if request["id"] != response["id"]: + raise Exception("Invalid RPC response: request {} response {}", request, response) + + return response_data + def get_operator_key(self): return self.signer.public_key() - - def get_account_info(self, storage_account): + def get_account_info(self, storage_account) -> AccountInfo: opts = { "encoding": "base64", "commitment": "confirmed", @@ -54,15 +82,39 @@ def get_account_info(self, storage_account): lamports = info['lamports'] owner = info['owner'] - return (account_tag, lamports, owner) + return AccountInfo(account_tag, lamports, owner) + def get_multiple_accounts_info(self, accounts: List[PublicKey]) -> List[AccountInfo]: + options = { + "encoding": "base64", + "commitment": "confirmed", + "dataSlice": { "offset": 0, "length": 16 } + } + result = self.client._provider.make_request("getMultipleAccounts", list(map(str, accounts)), options) + logger.debug("\n{}".format(json.dumps(result, indent=4, sort_keys=True))) + + if result['result']['value'] is None: + logger.debug("Can't get information about {}".format(accounts)) + return None + + accounts_info = [] + for info in result['result']['value']: + if info is None: + accounts_info.append(None) + else: + data = base64.b64decode(info['data'][0]) + accounts_info.append(AccountInfo(tag=data[0], lamports=info['lamports'], owner=info['owner'])) + + return accounts_info def get_sol_balance(self, account): return self.client.get_balance(account, commitment=Confirmed)['result']['value'] - def get_rent_exempt_balance_for_size(self, size): - return self.client.get_minimum_balance_for_rent_exemption(size, commitment=Confirmed)["result"] + def get_multiple_rent_exempt_balances_for_size(self, size_list: List[int]) -> List[int]: + request = map(lambda size: (size, {"commitment": "confirmed"}), size_list) + response = self._send_rpc_batch_request("getMinimumBalanceForRentExemption", request) + return list(map(lambda r: r["result"], response)) def _getAccountData(self, account, expected_length, owner=None): @@ -109,12 +161,23 @@ def send_transaction_unconfirmed(self, txn: Transaction): raise raise RuntimeError("Failed trying {} times to get Blockhash for transaction {}".format(RETRY_ON_FAIL, txn.__dict__)) + def send_multiple_transactions_unconfirmed(self, transactions: List[Transaction], skip_preflight: bool = True) -> List[str]: + blockhash_resp = self.client.get_recent_blockhash() # commitment=Confirmed + if not blockhash_resp["result"]: + raise RuntimeError("failed to get recent blockhash") - def collect_result(self, reciept, eth_trx, reason=None): - self.confirm_transaction(reciept) - result = self.client.get_confirmed_transaction(reciept) - update_transaction_cost(result, eth_trx, reason) - return result + blockhash = blockhash_resp["result"]["value"]["blockhash"] + + request = [] + for transaction in transactions: + transaction.recent_blockhash = blockhash + transaction.sign(self.signer) + + base64_transaction = base64.b64encode(transaction.serialize()).decode("utf-8") + request.append((base64_transaction, {"skipPreflight": skip_preflight, "encoding": "base64", "preflightCommitment": "confirmed"})) + + response = self._send_rpc_batch_request("sendTransaction", request) + return list(map(lambda r: r["result"], response)) def send_measured_transaction(self, trx, eth_trx, reason): @@ -134,30 +197,47 @@ def get_measurements(self, result): logger.error("Can't get measurements %s"%err) logger.info("Failed result: %s"%json.dumps(result, indent=3)) - - def confirm_transaction(self, tx_sig, confirmations=0): + def confirm_multiple_transactions(self, signatures: List[Union[str, bytes]]): """Confirm a transaction.""" TIMEOUT = 30 # 30 seconds pylint: disable=invalid-name elapsed_time = 0 while elapsed_time < TIMEOUT: - logger.debug('confirm_transaction for %s', tx_sig) - resp = self.client.get_signature_statuses([tx_sig]) - logger.debug('confirm_transaction: %s', resp) - if resp["result"]: - status = resp['result']['value'][0] - if status and (status['confirmationStatus'] == 'finalized' or \ - status['confirmationStatus'] == 'confirmed' and status['confirmations'] >= confirmations): - return + response = self.client.get_signature_statuses(signatures) + logger.debug('confirm_transactions: %s', response) + if response['result'] is None: + continue + + for status in response['result']['value']: + if status is None: + break + if status['confirmationStatus'] == 'processed': + break + else: + return + time.sleep(CONFIRMATION_CHECK_DELAY) elapsed_time += CONFIRMATION_CHECK_DELAY - raise RuntimeError("could not confirm transaction: ", tx_sig) + raise RuntimeError("could not confirm transactions: ", signatures) + + def get_multiple_confirmed_transactions(self, signatures: List[str]) -> List[RPCResponse]: + request = map(lambda signature: (signature, {"encoding": "json", "commitment": "confirmed"}), signatures) + return self._send_rpc_batch_request("getTransaction", request) - def collect_results(self, receipts, eth_trx=None, reason=None): - results = [] - for rcpt in receipts: - results.append(self.collect_result(rcpt, eth_trx, reason)) - return results + def collect_results(self, receipts: List[str], eth_trx: Any = None, reason: str = None) -> List[RPCResponse]: + self.confirm_multiple_transactions(receipts) + transactions = self.get_multiple_confirmed_transactions(receipts) + + for transaction in transactions: + update_transaction_cost(transaction, eth_trx, reason) + + return transactions + + def collect_result(self, reciept, eth_trx, reason=None): + self.confirm_multiple_transactions([reciept]) + result = self.client.get_confirmed_transaction(reciept) + update_transaction_cost(result, eth_trx, reason) + return result @staticmethod def extract_measurements_from_receipt(receipt): diff --git a/proxy/common_neon/transaction_sender.py b/proxy/common_neon/transaction_sender.py index c0f53bcc4..df384a8f7 100644 --- a/proxy/common_neon/transaction_sender.py +++ b/proxy/common_neon/transaction_sender.py @@ -2,6 +2,7 @@ import logging import math import os +from typing import List import rlp import time @@ -138,7 +139,7 @@ def create_account_with_seed(self, seed, storage_size): account = accountWithSeed(self.sender.get_operator_key(), seed) if self.sender.get_sol_balance(account) == 0: - minimum_balance = self.sender.get_rent_exempt_balance_for_size(storage_size) + minimum_balance = self.sender.get_multiple_rent_exempt_balances_for_size([storage_size])[0] logger.debug("Minimum balance required for account {}".format(minimum_balance)) trx = Transaction() @@ -148,29 +149,24 @@ def create_account_with_seed(self, seed, storage_size): return account - def create_multiple_accounts_with_seed(self, seeds, sizes): - accounts = [] - trx = Transaction() - - for seed, storage_size in zip(seeds, sizes): - account = accountWithSeed(self.sender.get_operator_key(), seed) - accounts.append(account) + def create_multiple_accounts_with_seed(self, seeds: List[bytes], sizes: List[int]) -> List[PublicKey]: + accounts = list(map(lambda seed: accountWithSeed(self.sender.get_operator_key(), seed), seeds)) + accounts_info = self.sender.get_multiple_accounts_info(accounts) + minimum_balances = self.sender.get_multiple_rent_exempt_balances_for_size(sizes) - minimum_balance = self.sender.get_rent_exempt_balance_for_size(storage_size) + trx = Transaction() - account_info = self.sender.get_account_info(account) + for account_key, account_info, seed, minimum_balance, storage_size in zip(accounts, accounts_info, seeds, minimum_balances, sizes): if account_info is None: logger.debug("Minimum balance required for account {}".format(minimum_balance)) - - trx.add(self.instruction.create_account_with_seed_trx(account, seed, minimum_balance, storage_size)) + trx.add(self.instruction.create_account_with_seed_trx(account_key, seed, minimum_balance, storage_size)) else: - (tag, lamports, owner) = account_info - if lamports < minimum_balance: + if account_info.lamports < minimum_balance: raise Exception("insufficient balance") - if PublicKey(owner) != PublicKey(EVM_LOADER_ID): + if PublicKey(account_info.owner) != PublicKey(EVM_LOADER_ID): raise Exception("wrong owner") - if tag not in {EMPTY_STORAGE_TAG, FINALIZED_STORAGE_TAG}: - raise Exception("not empty, not finalized") + if account_info.tag not in {EMPTY_STORAGE_TAG, FINALIZED_STORAGE_TAG}: + raise Exception("not empty, not finalized") if len(trx.instructions) > 0: self.sender.send_transaction(trx, eth_trx=self.eth_trx, reason='createAccountWithSeed') @@ -250,7 +246,7 @@ def create_account_list_by_emulate(self): code_account = accountWithSeed(self.sender.get_operator_key(), seed) logger.debug(" with code account %s", code_account) code_size = acc_desc["code_size"] + 2048 - code_account_balance = self.sender.get_rent_exempt_balance_for_size(code_size) + code_account_balance = self.sender.get_multiple_rent_exempt_balances_for_size([code_size])[0] self.create_acc_trx.add(self.instruction.create_account_with_seed_trx(code_account, seed, code_account_balance, code_size)) # add_keys_05.append(AccountMeta(pubkey=code_account, is_signer=False, is_writable=acc_desc["writable"])) code_account_writable = acc_desc["writable"] @@ -343,32 +339,29 @@ def __init__(self, solana_interactor: SolanaInteractor, neon_instruction: NeonIn def call_signed_iterative_combined(self): - self.create_accounts_for_trx() + if len(self.create_acc_trx.instructions) > 0: + create_accounts_siganture = self.sender.send_transaction_unconfirmed(self.create_acc_trx) + self.sender.confirm_multiple_transactions([create_accounts_siganture]) + self.create_acc_trx = Transaction() + self.instruction_type = self.CONTINUE_COMBINED return self.call_continue() def call_signed_with_holder_combined(self): - self.write_trx_to_holder_account() - self.create_accounts_for_trx() - self.instruction_type = self.CONTINUE_HOLDER_COMB - return self.call_continue() - + precall_transactions = self.make_write_to_holder_account_trx() + if len(self.create_acc_trx.instructions) > 0: + precall_transactions.append(self.create_acc_trx) + signatures = self.sender.send_multiple_transactions_unconfirmed(precall_transactions) + self.sender.confirm_multiple_transactions(signatures) - def create_accounts_for_trx(self): - length = len(self.create_acc_trx.instructions) - if length == 0: - return - logger.debug(f"Create account for trx: {length}") - precall_txs = Transaction() - precall_txs.add(self.create_acc_trx) - result = self.sender.send_measured_transaction(precall_txs, self.eth_trx, 'CreateAccountsForTrx') - if check_for_errors(result): - raise Exception("Failed to create account for trx") self.create_acc_trx = Transaction() + self.instruction_type = self.CONTINUE_HOLDER_COMB + return self.call_continue() + - def write_trx_to_holder_account(self): + def make_write_to_holder_account_trx(self) -> List[Transaction]: logger.debug('write_trx_to_holder_account') msg = self.eth_trx.signature() + len(self.eth_trx.unsigned_msg()).to_bytes(8, byteorder="little") + self.eth_trx.unsigned_msg() @@ -381,19 +374,7 @@ def write_trx_to_holder_account(self): trxs.append(trx) offset += len(part) - while len(trxs) > 0: - receipts = {} - for trx in trxs: - receipts[self.sender.send_transaction_unconfirmed(trx)] = trx - - logger.debug("receipts %s", receipts) - for rcpt, trx in receipts.items(): - try: - self.sender.collect_result(rcpt, eth_trx=self.eth_trx, reason='WriteHolder') - except Exception as err: - logger.debug("collect_result exception: {}".format(str(err))) - else: - trxs.remove(trx) + return trxs def call_continue(self): From 9b7f8325f218048b1ad1dcc88aa7344ad6d57bda Mon Sep 17 00:00:00 2001 From: Vasiliy Zaznobin Date: Tue, 28 Dec 2021 11:13:27 +0300 Subject: [PATCH 13/14] NEON_PROXY_PKG_VERSION = '0.5.2' --- proxy/plugin/solana_rest_api.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/plugin/solana_rest_api.py b/proxy/plugin/solana_rest_api.py index 983a8958b..8c2e7142f 100644 --- a/proxy/plugin/solana_rest_api.py +++ b/proxy/plugin/solana_rest_api.py @@ -47,7 +47,7 @@ modelInstanceLock = threading.Lock() modelInstance = None -NEON_PROXY_PKG_VERSION = '0.5.2-dev' +NEON_PROXY_PKG_VERSION = '0.5.2' NEON_PROXY_REVISION = 'NEON_PROXY_REVISION_TO_BE_REPLACED' diff --git a/setup.py b/setup.py index 7d8e87305..bcec2bcb8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ """ from setuptools import setup, find_packages -VERSION = (0, 5, 1) +VERSION = (0, 5, 2) __version__ = '.'.join(map(str, VERSION[0:3])) __description__ = '''⚡⚡⚡Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on Network monitoring, controls & Application development, testing, debugging.''' From dc03edc13530a984c12d05b1e3da9c433d480d64 Mon Sep 17 00:00:00 2001 From: Vasiliy Zaznobin Date: Tue, 28 Dec 2021 11:32:01 +0300 Subject: [PATCH 14/14] latest->stable --- .buildkite/steps/build-image.sh | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.buildkite/steps/build-image.sh b/.buildkite/steps/build-image.sh index a67344b9f..104ffc621 100755 --- a/.buildkite/steps/build-image.sh +++ b/.buildkite/steps/build-image.sh @@ -4,7 +4,7 @@ set -euo pipefail REVISION=$(git rev-parse HEAD) set ${SOLANA_REVISION:=v1.7.9-testnet} -set ${EVM_LOADER_REVISION:=latest} +set ${EVM_LOADER_REVISION:=stable} # Refreshing neonlabsorg/solana:latest image is required to run .buildkite/steps/build-image.sh locally docker pull neonlabsorg/solana:${SOLANA_REVISION} diff --git a/Dockerfile b/Dockerfile index a1abf84f3..19cbbe0e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG SOLANA_REVISION=v1.7.9-testnet -ARG EVM_LOADER_REVISION=latest +ARG EVM_LOADER_REVISION=stable FROM neonlabsorg/solana:${SOLANA_REVISION} AS cli