|
| 1 | +from proxy.indexer.indexer_base import IndexerBase, logger |
| 2 | +import os |
| 3 | +import requests |
| 4 | +import base58 |
| 5 | +import json |
| 6 | +import logging |
| 7 | + |
| 8 | +try: |
| 9 | + from utils import check_error |
| 10 | + from sql_dict import SQLDict |
| 11 | +except ImportError: |
| 12 | + from .utils import check_error |
| 13 | + from .sql_dict import SQLDict |
| 14 | + |
| 15 | +class Airdropper(IndexerBase): |
| 16 | + def __init__(self, |
| 17 | + solana_url, |
| 18 | + evm_loader_id, |
| 19 | + faucet_url = '', |
| 20 | + wrapper_whitelist = [], |
| 21 | + airdrop_amount = 10, |
| 22 | + log_level = 'INFO'): |
| 23 | + IndexerBase.__init__(self, solana_url, evm_loader_id, log_level) |
| 24 | + |
| 25 | + # collection of eth-address-to-create-accout-trx mappings |
| 26 | + # for every addresses that was already funded with airdrop |
| 27 | + self.airdrop_ready = SQLDict(tablename="airdrop_ready") |
| 28 | + self.wrapper_whitelist = wrapper_whitelist |
| 29 | + self.airdrop_amount = airdrop_amount |
| 30 | + self.faucet_url = faucet_url |
| 31 | + |
| 32 | + |
| 33 | + # helper function checking if given contract address is in whitelist |
| 34 | + def _is_allowed_wrapper_contract(self, contract_addr): |
| 35 | + return contract_addr in self.wrapper_whitelist |
| 36 | + |
| 37 | + |
| 38 | + # helper function checking if given 'create account' corresponds to 'create erc20 token account' instruction |
| 39 | + def _check_create_instr(self, account_keys, create_acc, create_token_acc): |
| 40 | + # Must use the same Ethereum account |
| 41 | + if account_keys[create_acc['accounts'][1]] != account_keys[create_token_acc['accounts'][2]]: |
| 42 | + return False |
| 43 | + # Must use the same token program |
| 44 | + if account_keys[create_acc['accounts'][5]] != account_keys[create_token_acc['accounts'][6]]: |
| 45 | + return False |
| 46 | + # Token program must be system token program |
| 47 | + if account_keys[create_acc['accounts'][5]] != 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA': |
| 48 | + return False |
| 49 | + # CreateERC20TokenAccount instruction must use ERC20-wrapper from whitelist |
| 50 | + if not self._is_allowed_wrapper_contract(account_keys[create_token_acc['accounts'][3]]): |
| 51 | + return False |
| 52 | + return True |
| 53 | + |
| 54 | + |
| 55 | + # helper function checking if given 'create erc20 token account' corresponds to 'token transfer' instruction |
| 56 | + def _check_transfer(self, account_keys, create_token_acc, token_transfer) -> bool: |
| 57 | + return account_keys[create_token_acc['accounts'][1]] == account_keys[token_transfer['accounts'][1]] |
| 58 | + |
| 59 | + |
| 60 | + def _airdrop_to(self, create_acc): |
| 61 | + eth_address = "0x" + bytearray(base58.b58decode(create_acc['data'])[20:][:20]).hex() |
| 62 | + |
| 63 | + if eth_address in self.airdrop_ready: # transaction already processed |
| 64 | + return |
| 65 | + |
| 66 | + logger.info(f"Airdrop to address: {eth_address}") |
| 67 | + |
| 68 | + json_data = { 'wallet': eth_address, 'amount': self.airdrop_amount } |
| 69 | + resp = requests.post(self.faucet_url + '/request_eth_token', json = json_data) |
| 70 | + if not resp.ok: |
| 71 | + logger.warning(f'Failed to airdrop: {resp.status_code}') |
| 72 | + return |
| 73 | + |
| 74 | + self.airdrop_ready[eth_address] = create_acc |
| 75 | + |
| 76 | + |
| 77 | + def process_trx_airdropper_mode(self, trx): |
| 78 | + if check_error(trx): |
| 79 | + return |
| 80 | + |
| 81 | + # helper function finding all instructions that satisfies predicate |
| 82 | + def find_instructions(trx, predicate): |
| 83 | + return [instr for instr in trx['transaction']['message']['instructions'] if predicate(instr)] |
| 84 | + |
| 85 | + account_keys = trx["transaction"]["message"]["accountKeys"] |
| 86 | + |
| 87 | + # Finding instructions specific for airdrop. |
| 88 | + # Airdrop triggers on sequence: |
| 89 | + # neon.CreateAccount -> neon.CreateERC20TokenAccount -> spl.Transfer (maybe shuffled) |
| 90 | + # First: select all instructions that can form such chains |
| 91 | + predicate = lambda instr: account_keys[instr['programIdIndex']] == self.evm_loader_id \ |
| 92 | + and base58.b58decode(instr['data'])[0] == 0x02 |
| 93 | + create_acc_list = find_instructions(trx, predicate) |
| 94 | + |
| 95 | + predicate = lambda instr: account_keys[instr['programIdIndex']] == self.evm_loader_id \ |
| 96 | + and base58.b58decode(instr['data'])[0] == 0x0f |
| 97 | + create_token_acc_list = find_instructions(trx, predicate) |
| 98 | + |
| 99 | + predicate = lambda instr: account_keys[instr['programIdIndex']] == 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' \ |
| 100 | + and base58.b58decode(instr['data'])[0] == 0x03 |
| 101 | + token_transfer_list = find_instructions(trx, predicate) |
| 102 | + |
| 103 | + # Second: Find exact chains of instructions in sets created previously |
| 104 | + for create_acc in create_acc_list: |
| 105 | + for create_token_acc in create_token_acc_list: |
| 106 | + if not self._check_create_instr(account_keys, create_acc, create_token_acc): |
| 107 | + continue |
| 108 | + for token_transfer in token_transfer_list: |
| 109 | + if not self._check_transfer(account_keys, create_token_acc, token_transfer): |
| 110 | + continue |
| 111 | + self._airdrop_to(create_acc) |
| 112 | + |
| 113 | + |
| 114 | + def process_functions(self): |
| 115 | + IndexerBase.process_functions(self) |
| 116 | + logger.debug("Process receipts") |
| 117 | + self.process_receipts() |
| 118 | + |
| 119 | + |
| 120 | + def process_receipts(self): |
| 121 | + counter = 0 |
| 122 | + for signature in self.transaction_order: |
| 123 | + counter += 1 |
| 124 | + if signature in self.transaction_receipts: |
| 125 | + trx = self.transaction_receipts[signature] |
| 126 | + if trx is None: |
| 127 | + logger.error("trx is None") |
| 128 | + del self.transaction_receipts[signature] |
| 129 | + continue |
| 130 | + if 'slot' not in trx: |
| 131 | + logger.debug("\n{}".format(json.dumps(trx, indent=4, sort_keys=True))) |
| 132 | + exit() |
| 133 | + if trx['transaction']['message']['instructions'] is not None: |
| 134 | + self.process_trx_airdropper_mode(trx) |
| 135 | + |
| 136 | + |
| 137 | +def run_airdropper(solana_url, |
| 138 | + evm_loader_id, |
| 139 | + faucet_url = '', |
| 140 | + wrapper_whitelist = [], |
| 141 | + airdrop_amount = 10, |
| 142 | + log_level = 'INFO'): |
| 143 | + logging.basicConfig(format='%(asctime)s - pid:%(process)d [%(levelname)-.1s] %(funcName)s:%(lineno)d - %(message)s') |
| 144 | + logger.setLevel(logging.DEBUG) |
| 145 | + logger.info(f"""Running indexer with params: |
| 146 | + solana_url: {solana_url}, |
| 147 | + evm_loader_id: {evm_loader_id}, |
| 148 | + log_level: {log_level}, |
| 149 | + faucet_url: {faucet_url}, |
| 150 | + wrapper_whitelist: {wrapper_whitelist}, |
| 151 | + airdrop_amount: {airdrop_amount}""") |
| 152 | + |
| 153 | + airdropper = Airdropper(solana_url, |
| 154 | + evm_loader_id, |
| 155 | + faucet_url, |
| 156 | + wrapper_whitelist, |
| 157 | + airdrop_amount, |
| 158 | + log_level) |
| 159 | + airdropper.run() |
0 commit comments