Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5a3e9a6
Use neonlabsorg/evm_loader:stable image for build
Oct 12, 2021
e59efe2
Merge remote-tracking branch 'origin/develop'
Oct 19, 2021
44dd154
Merge remote-tracking branch 'origin/develop'
Oct 21, 2021
21d20a1
Launch all uniswap tests for master branch
vasiliy-zaznobin Nov 16, 2021
4e03e73
Merge remote-tracking branch 'origin/develop'
Nov 16, 2021
e496b27
VERSION = (0, 4, 0)
vasiliy-zaznobin Nov 16, 2021
fe41d64
EVM_LOADER_REVISION:=v0.4.0
vasiliy-zaznobin Nov 16, 2021
5467c50
EVM_LOADER_REVISION:=stable
vasiliy-zaznobin Nov 16, 2021
e15d847
Merge remote-tracking branch 'origin/develop'
vasiliy-zaznobin Dec 7, 2021
2416591
NEON_PROXY_PKG_VERSION = '0.5.0'
vasiliy-zaznobin Dec 7, 2021
168e428
VERSION = (0, 4, 0)
vasiliy-zaznobin Nov 16, 2021
3125645
EVM_LOADER_REVISION:=v0.4.0
vasiliy-zaznobin Nov 16, 2021
07f175d
EVM_LOADER_REVISION:=stable
vasiliy-zaznobin Nov 16, 2021
0bbb26b
289 implement eth get storage at (#298)
ivandzen Nov 18, 2021
7841b07
#256 create and airdrop eth account (#259)
rozhkovdmitrii Nov 18, 2021
ce05c81
#305 Remove extra args from ether2program (#309)
rozhkovdmitrii Nov 19, 2021
6ca860a
Change default logDB postgress pass (#317)
rozhkovdmitrii Nov 23, 2021
b3b322f
#311 Create account and airdrop on gas estimation if it's preset
rozhkovdmitrii Nov 24, 2021
c860ed0
neonbals/neon-evm#371 Update STORAGE_ACCOUNT_INFO_LAYOUT (#315)
sinev-valentine Nov 24, 2021
9bb3f89
319 add neon cli version handler (#325)
vasiliy-zaznobin Nov 24, 2021
081bc75
fix storage account check (#327)
sinev-valentine Nov 24, 2021
7845ca9
#320 add neon proxy version handler (#323)
vasiliy-zaznobin Nov 25, 2021
f299c57
313 concurrent execution of solana program dump (#314)
ivandzen Nov 25, 2021
0a5dc2d
fix scripts (#329)
ivandzen Nov 25, 2021
7964cad
#318 JSON_RPC "params" field may be omitted (#322)
mich-master Nov 26, 2021
01df8a4
#336 indexer refactoring (#340)
ivandzen Dec 2, 2021
d9a8e51
#333 fix indexer errors (#334)
otselnik Dec 2, 2021
f4b178c
#291 Proxy refactoring (#324)
otselnik Dec 2, 2021
5ced809
#337 сreate base airdropper service (#343)
ivandzen Dec 3, 2021
ae7b392
#337 fix running airdropper (#347)
ivandzen Dec 6, 2021
9feca6a
#351 fix canceller droping running transactions (#352)
otselnik Dec 6, 2021
2f0a0df
349 improve neon proxy logging to filter a request and the correspond…
vasiliy-zaznobin Dec 6, 2021
3b36864
#349 Fix using log sending solana transaction (#353)
vasiliy-zaznobin Dec 6, 2021
6bb8509
#295 iterative execution (#332)
otselnik Dec 6, 2021
56044ea
#354 Check result for errors (#355)
otselnik Dec 7, 2021
0bad34c
#360 pass transaction too large upper (#361)
otselnik Dec 7, 2021
9e8344c
NEON_PROXY_PKG_VERSION = '0.5.0'
vasiliy-zaznobin Dec 7, 2021
981c178
Merge remote-tracking branch 'origin/develop->master_0.5.0' into deve…
vasiliy-zaznobin Dec 7, 2021
4c87c95
Merge pull request #362 from neonlabsorg/develop->master_0.5.0
vasiliy-zaznobin Dec 7, 2021
2ccfcce
Merge remote-tracking branch 'origin/develop' into develop->master_0.…
vasiliy-zaznobin Dec 8, 2021
28881d2
Merge pull request #363 from neonlabsorg/develop->master_0.5.0-rc2
vasiliy-zaznobin Dec 8, 2021
3771577
Merge remote-tracking branch 'origin/develop' into develop->master_0.…
vasiliy-zaznobin Dec 13, 2021
1e4c4ab
Merge pull request #372 from neonlabsorg/develop->master_0.5.0_rc3
vasiliy-zaznobin Dec 13, 2021
cacf6f8
Master 0.5.0 rc4 (#380)
vasiliy-zaznobin Dec 16, 2021
5ce4d3d
0.5.0-rc4 (#386)
vasiliy-zaznobin Dec 16, 2021
6a3dbd9
Neon-proxy/v0.5.0 (#388)
vasiliy-zaznobin Dec 17, 2021
424cf24
Merge remote-tracking branch 'origin/master' into master_v0.5.0->develop
vasiliy-zaznobin Dec 17, 2021
5aef94b
Neon-proxy/v0.5.1-dev
vasiliy-zaznobin Dec 17, 2021
03c051b
stable->latest
vasiliy-zaznobin Dec 17, 2021
3e43911
VERSION = (0, 5, 1)
vasiliy-zaznobin Dec 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
#337 сreate base airdropper service (#343)
* 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

Co-authored-by: ivanl <[email protected]>
  • Loading branch information
2 people authored and vasiliy-zaznobin committed Dec 7, 2021
commit 5ced8095583bfb34d379c9da843286fc27aa3f3b
175 changes: 175 additions & 0 deletions proxy/indexer/airdropper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from proxy.indexer.indexer_base import IndexerBase, logger
import os
import requests
import base58
import json
import logging

try:
from utils import check_error
from sql_dict import SQLDict
except ImportError:
from .utils import check_error
from .sql_dict import SQLDict

class Airdropper(IndexerBase):
def __init__(self,
solana_url,
evm_loader_id,
faucet_url = '',
wrapper_whitelist = [],
airdrop_amount = 10,
log_level = 'INFO'):
IndexerBase.__init__(self, solana_url, evm_loader_id, log_level)

# collection of eth-address-to-create-accout-trx mappings
# for every addresses that was already funded with airdrop
self.airdrop_ready = SQLDict(tablename="airdrop_ready")
self.wrapper_whitelist = wrapper_whitelist
self.airdrop_amount = airdrop_amount
self.faucet_url = faucet_url


# helper function checking if given contract address is in whitelist
def _is_allowed_wrapper_contract(self, contract_addr):
return contract_addr in self.wrapper_whitelist


# helper function checking if given 'create account' corresponds to 'create erc20 token account' instruction
def _check_create_instr(self, account_keys, create_acc, create_token_acc):
# Must use the same Ethereum account
if account_keys[create_acc['accounts'][1]] != account_keys[create_token_acc['accounts'][2]]:
return False
# Must use the same token program
if account_keys[create_acc['accounts'][5]] != account_keys[create_token_acc['accounts'][6]]:
return False
# Token program must be system token program
if account_keys[create_acc['accounts'][5]] != 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA':
return False
# CreateERC20TokenAccount instruction must use ERC20-wrapper from whitelist
if not self._is_allowed_wrapper_contract(account_keys[create_token_acc['accounts'][3]]):
return False
return True


# helper function checking if given 'create erc20 token account' corresponds to 'token transfer' instruction
def _check_transfer(self, account_keys, create_token_acc, token_transfer) -> bool:
return account_keys[create_token_acc['accounts'][1]] == account_keys[token_transfer['accounts'][1]]


def _airdrop_to(self, create_acc):
eth_address = "0x" + bytearray(base58.b58decode(create_acc['data'])[20:][:20]).hex()

if eth_address in self.airdrop_ready: # transaction already processed
return

logger.info(f"Airdrop to address: {eth_address}")

json_data = { 'wallet': eth_address, 'amount': self.airdrop_amount }
resp = requests.post(self.faucet_url + '/request_eth_token', json = json_data)
if not resp.ok:
logger.warning(f'Failed to airdrop: {resp.status_code}')
return

self.airdrop_ready[eth_address] = create_acc


def process_trx_airdropper_mode(self, trx):
if check_error(trx):
return

# helper function finding all instructions that satisfies predicate
def find_instructions(trx, predicate):
return [instr for instr in trx['transaction']['message']['instructions'] if predicate(instr)]

account_keys = trx["transaction"]["message"]["accountKeys"]

# Finding instructions specific for airdrop.
# Airdrop triggers on sequence:
# neon.CreateAccount -> neon.CreateERC20TokenAccount -> spl.Transfer (maybe shuffled)
# First: select all instructions that can form such chains
predicate = lambda instr: account_keys[instr['programIdIndex']] == self.evm_loader_id \
and base58.b58decode(instr['data'])[0] == 0x02
create_acc_list = find_instructions(trx, predicate)

predicate = lambda instr: account_keys[instr['programIdIndex']] == self.evm_loader_id \
and base58.b58decode(instr['data'])[0] == 0x0f
create_token_acc_list = find_instructions(trx, predicate)

predicate = lambda instr: account_keys[instr['programIdIndex']] == 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' \
and base58.b58decode(instr['data'])[0] == 0x03
token_transfer_list = find_instructions(trx, predicate)

# Second: Find exact chains of instructions in sets created previously
for create_acc in create_acc_list:
for create_token_acc in create_token_acc_list:
if not self._check_create_instr(account_keys, create_acc, create_token_acc):
continue
for token_transfer in token_transfer_list:
if not self._check_transfer(account_keys, create_token_acc, token_transfer):
continue
self._airdrop_to(create_acc)


def process_functions(self):
IndexerBase.process_functions(self)
logger.debug("Process receipts")
self.process_receipts()


def process_receipts(self):
counter = 0
for signature in self.transaction_order:
counter += 1
if signature in self.transaction_receipts:
trx = self.transaction_receipts[signature]
if trx is None:
logger.error("trx is None")
del self.transaction_receipts[signature]
continue
if 'slot' not in trx:
logger.debug("\n{}".format(json.dumps(trx, indent=4, sort_keys=True)))
exit()
if trx['transaction']['message']['instructions'] is not None:
self.process_trx_airdropper_mode(trx)


def run_airdropper(solana_url,
evm_loader_id,
faucet_url = '',
wrapper_whitelist = [],
airdrop_amount = 10,
log_level = 'INFO'):
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:
solana_url: {solana_url},
evm_loader_id: {evm_loader_id},
log_level: {log_level},
faucet_url: {faucet_url},
wrapper_whitelist: {wrapper_whitelist},
airdrop_amount: {airdrop_amount}""")

airdropper = Airdropper(solana_url,
evm_loader_id,
faucet_url,
wrapper_whitelist,
airdrop_amount,
log_level)
airdropper.run()


if __name__ == "__main__":
solana_url = os.environ.get('SOLANA_URL', 'http://localhost:8899')
evm_loader_id = os.environ.get('EVM_LOADER_ID', '53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io')
faucet_url = os.environ.get('FAUCET_URL', 'http://localhost:3333')
wrapper_whitelist = os.environ.get('INDEXER_ERC20_WRAPPER_WHITELIST', '').split(',')
airdrop_amount = os.environ.get('AIRDROP_AMOUNT', 0)
log_level = os.environ.get('LOG_LEVEL', 'INFO')

run_airdropper(solana_url,
evm_loader_id,
faucet_url,
wrapper_whitelist,
airdrop_amount,
log_level)
28 changes: 28 additions & 0 deletions proxy/testing/mock_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import requests

from flask import Flask, request
from threading import Thread

class MockServer(Thread):
def __init__(self, port):
super().__init__()
self.port = port
self.app = Flask(__name__)
self.url = "http://localhost:%s" % self.port
self.app.add_url_rule("/shutdown", view_func=self._shutdown_server)

def add_url_rule(self, url, callback, methods):
self.app.add_url_rule(url, view_func=callback, methods=methods)

def _shutdown_server(self):
if not 'werkzeug.server.shutdown' in request.environ:
raise RuntimeError('Not running the development server')
request.environ['werkzeug.server.shutdown']()
return 'Server shutting down...'

def run(self):
self.app.run(port=self.port)

def shutdown_server(self):
requests.get("http://localhost:%s/shutdown" % self.port)
self.join()
176 changes: 176 additions & 0 deletions proxy/testing/test_airdropper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import unittest
from proxy.testing.mock_server import MockServer
from proxy.indexer.airdropper import Airdropper
from proxy.indexer.sql_dict import SQLDict
import time
from flask import request, Response
from unittest.mock import MagicMock, patch, call, ANY
import itertools
from proxy.testing.transactions import pre_token_airdrop_trx1, pre_token_airdrop_trx2,\
create_sol_acc_and_airdrop_trx, wrapper_whitelist, evm_loader_addr, token_airdrop_address1, \
token_airdrop_address2, token_airdrop_address3

class MockFaucet(MockServer):
def __init__(self, port):
super().__init__(port)
self.request_eth_token_mock = MagicMock()
self.request_eth_token_mock.side_effect = itertools.repeat({})
self.add_url_rule("/request_eth_token", callback=self.request_eth_token, methods=['POST'])

def request_eth_token(self):
req = request.get_json()
return self.request_eth_token_mock(req)


def create_signature_for_address(signature: str):
return {
'blockTime': 1638177745, # not make sense
'confirmationStatus': 'finalized',
'err': None,
'memo': None,
'signature': signature,
'slot': 9748200 # not make sense
}


def create_get_signatures_for_address(signatures: list):
return {
'jsonrpc': '2.0',
'result': [ create_signature_for_address(sign) for sign in signatures ],
'id': 1
}


class Test_Airdropper(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print("testing indexer in airdropper mode")
cls.address = 'localhost'
cls.faucet_port = 3333
cls.airdrop_amount = 10

cls.faucet = MockFaucet(cls.faucet_port)
cls.faucet.start()
time.sleep(0.2)

cls.evm_loader_id = evm_loader_addr
cls.wrapper_whitelist = wrapper_whitelist
cls.airdropper = Airdropper(f'http://{cls.address}:8899',
cls.evm_loader_id,
f'http://{cls.address}:{cls.faucet_port}',
cls.wrapper_whitelist,
cls.airdrop_amount,
'INFO')


@classmethod
def tearDownClass(cls) -> None:
cls.faucet.shutdown_server()
cls.faucet.join()


@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_success_process_trx_with_one_airdrop(self,
mock_sql_dict_contains,
mock_sql_dict_setitem):
print("\n\nShould airdrop to new address - one target in transaction")
mock_sql_dict_contains.side_effect = [False] # new eth address
self.faucet.request_eth_token_mock.side_effect = [Response("{}", status=200, mimetype='application/json')]

self.airdropper.process_trx_airdropper_mode(pre_token_airdrop_trx1)

mock_sql_dict_contains.assert_called_once_with(token_airdrop_address1)
mock_sql_dict_setitem.assert_has_calls([call(token_airdrop_address1, ANY)])
json_req = {'wallet': token_airdrop_address1, 'amount': self.airdrop_amount}
self.faucet.request_eth_token_mock.assert_called_once_with(json_req)
self.faucet.request_eth_token_mock.reset_mock()


@patch.object(Airdropper, '_is_allowed_wrapper_contract')
@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_failed_airdrop_contract_not_in_whitelist(self,
mock_sql_dict_contains,
mock_sql_dict_setitem,
mock_is_allowed_contract):
print("\n\nShould not airdrop for contract that is not in whitelist")
mock_is_allowed_contract.side_effect = [False]
self.airdropper.process_trx_airdropper_mode(pre_token_airdrop_trx1)

mock_is_allowed_contract.assert_called_once()
mock_sql_dict_contains.assert_not_called()
mock_sql_dict_setitem.assert_not_called()
self.faucet.request_eth_token_mock.assert_not_called()
self.faucet.request_eth_token_mock.reset_mock()


@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_faucet_failure(self,
mock_sql_dict_contains,
mock_sql_dict_setitem):
print("\n\nShould not add address to processed list due to faucet error")
mock_sql_dict_contains.side_effect = [False] # new eth address
self.faucet.request_eth_token_mock.side_effect = [Response("{}", status=400, mimetype='application/json')]

self.airdropper.process_trx_airdropper_mode(pre_token_airdrop_trx1)

mock_sql_dict_contains.assert_called_once_with(token_airdrop_address1)
mock_sql_dict_setitem.assert_not_called()
json_req = {'wallet': token_airdrop_address1, 'amount': self.airdrop_amount}
self.faucet.request_eth_token_mock.assert_called_once_with(json_req)
self.faucet.request_eth_token_mock.reset_mock()


@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_process_trx_with_one_airdrop_for_already_processed_address(self,
mock_sql_dict_contains,
mock_sql_dict_setitem):
print("\n\nShould not airdrop to repeated address")
mock_sql_dict_contains.side_effect = [True] # eth address processed later

self.airdropper.process_trx_airdropper_mode(pre_token_airdrop_trx1)

mock_sql_dict_contains.assert_called_once_with(token_airdrop_address1)
mock_sql_dict_setitem.assert_not_called()
self.faucet.request_eth_token_mock.assert_not_called()
self.faucet.request_eth_token_mock.reset_mock()


@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_complex_transation(self,
mock_sql_dict_contains,
mock_sql_dict_setitem):
print("\n\nShould airdrop to several targets in one transaction")
mock_sql_dict_contains.side_effect = [False, False] # both targets are new
self.faucet.request_eth_token_mock.side_effect = [Response("{}", status=200, mimetype='application/json'),
Response("{}", status=200, mimetype='application/json')]

self.airdropper.process_trx_airdropper_mode(pre_token_airdrop_trx2)

mock_sql_dict_contains.assert_has_calls([call(token_airdrop_address3),
call(token_airdrop_address2)])
mock_sql_dict_setitem.assert_has_calls([call(token_airdrop_address3, ANY),
call(token_airdrop_address2, ANY)])
json_req1 = {'wallet': token_airdrop_address2, 'amount': self.airdrop_amount}
json_req2 = {'wallet': token_airdrop_address3, 'amount': self.airdrop_amount}
self.faucet.request_eth_token_mock.assert_has_calls([call(json_req2), call(json_req1)])
self.faucet.request_eth_token_mock.reset_mock()


@patch.object(SQLDict, '__setitem__')
@patch.object(SQLDict, '__contains__')
def test_no_airdrop_instructions(self,
mock_sql_dict_contains,
mock_sql_dict_setitem):
print("\n\nShould not airdrop when instructions are inconsistent")
self.airdropper.process_trx_airdropper_mode(create_sol_acc_and_airdrop_trx)

mock_sql_dict_contains.assert_not_called()
mock_sql_dict_setitem.assert_not_called()
self.faucet.request_eth_token_mock.assert_not_called()
self.faucet.request_eth_token_mock.reset_mock()

Loading