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