2021-02-18 06:04:30 +01:00
# standard imports
import sys
import os
import argparse
import logging
import time
import enum
import re
# third-party imports
import confini
from cic_registry import CICRegistry
from cic_registry . chain import (
ChainRegistry ,
ChainSpec ,
)
#from cic_registry.bancor import BancorRegistryClient
from cic_registry . token import Token
from cic_registry . error import (
UnknownContractError ,
UnknownDeclarationError ,
)
from cic_registry . declaration import to_token_declaration
from web3 . exceptions import BlockNotFound , TransactionNotFound
from websockets . exceptions import ConnectionClosedError
from requests . exceptions import ConnectionError
import web3
from web3 import HTTPProvider , WebsocketProvider
# local imports
from cic_cache import db
from cic_cache . db . models . base import SessionBase
logging . basicConfig ( level = logging . WARNING )
logg = logging . getLogger ( )
logging . getLogger ( ' websockets.protocol ' ) . setLevel ( logging . CRITICAL )
logging . getLogger ( ' urllib3 ' ) . setLevel ( logging . CRITICAL )
logging . getLogger ( ' web3.RequestManager ' ) . setLevel ( logging . CRITICAL )
logging . getLogger ( ' web3.providers.WebsocketProvider ' ) . setLevel ( logging . CRITICAL )
logging . getLogger ( ' web3.providers.HTTPProvider ' ) . setLevel ( logging . CRITICAL )
log_topics = {
' transfer ' : ' 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef ' ,
' convert ' : ' 0x7154b38b5dd31bb3122436a96d4e09aba5b323ae1fd580025fab55074334c095 ' ,
' accountregistry_add ' : ' 0a3b0a4f4c6e53dce3dbcad5614cb2ba3a0fa7326d03c5d64b4fa2d565492737 ' ,
}
config_dir = os . path . join ( ' /usr/local/etc/cic-cache ' )
argparser = argparse . ArgumentParser ( description = ' daemon that monitors transactions in new blocks ' )
argparser . add_argument ( ' -c ' , type = str , default = config_dir , help = ' config root to use ' )
argparser . add_argument ( ' -i ' , ' --chain-spec ' , type = str , dest = ' i ' , help = ' chain spec ' )
argparser . add_argument ( ' --trust-address ' , default = [ ] , type = str , dest = ' trust_address ' , action = ' append ' , help = ' Set address as trust ' )
argparser . add_argument ( ' --env-prefix ' , default = os . environ . get ( ' CONFINI_ENV_PREFIX ' ) , dest = ' env_prefix ' , type = str , help = ' environment prefix for variables to overwrite configuration ' )
argparser . add_argument ( ' --abi-dir ' , dest = ' abi_dir ' , type = str , help = ' Directory containing bytecode and abi ' )
argparser . add_argument ( ' -v ' , help = ' be verbose ' , action = ' store_true ' )
argparser . add_argument ( ' -vv ' , help = ' be more verbose ' , action = ' store_true ' )
args = argparser . parse_args ( sys . argv [ 1 : ] )
config_dir = os . path . join ( args . c )
os . makedirs ( config_dir , 0o777 , True )
if args . v == True :
logging . getLogger ( ) . setLevel ( logging . INFO )
elif args . vv == True :
logging . getLogger ( ) . setLevel ( logging . DEBUG )
config = confini . Config ( config_dir , args . env_prefix )
config . process ( )
args_override = {
' ETH_ABI_DIR ' : getattr ( args , ' abi_dir ' ) ,
' CIC_TRUST_ADDRESS ' : " , " . join ( getattr ( args , ' trust_address ' , [ ] ) ) ,
}
config . dict_override ( args_override , ' cli flag ' )
config . censor ( ' PASSWORD ' , ' DATABASE ' )
config . censor ( ' PASSWORD ' , ' SSL ' )
logg . debug ( ' config loaded from {} : \n {} ' . format ( config_dir , config ) )
# connect to database
dsn = db . dsn_from_config ( config )
SessionBase . connect ( dsn )
re_websocket = re . compile ( ' ^wss?:// ' )
re_http = re . compile ( ' ^https?:// ' )
blockchain_provider = config . get ( ' ETH_PROVIDER ' )
if re . match ( re_websocket , blockchain_provider ) != None :
blockchain_provider = WebsocketProvider ( blockchain_provider )
elif re . match ( re_http , blockchain_provider ) != None :
blockchain_provider = HTTPProvider ( blockchain_provider )
else :
raise ValueError ( ' unknown provider url {} ' . format ( blockchain_provider ) )
def web3_constructor ( ) :
w3 = web3 . Web3 ( blockchain_provider )
return ( blockchain_provider , w3 )
class RunStateEnum ( enum . IntEnum ) :
INIT = 0
RUN = 1
TERMINATE = 9
def rubberstamp ( src ) :
return True
class Tracker :
def __init__ ( self , chain_spec , trusts = [ ] ) :
self . block_height = 0
self . tx_height = 0
self . state = RunStateEnum . INIT
self . declarator_cache = { }
self . convert_enabled = False
self . trusts = trusts
self . chain_spec = chain_spec
self . declarator = CICRegistry . get_contract ( chain_spec , ' AddressDeclarator ' , ' Declarator ' )
def __process_tx ( self , w3 , session , t , r , l , b ) :
token_value = int ( l . data , 16 )
token_sender = l . topics [ 1 ] [ - 20 : ] . hex ( )
token_recipient = l . topics [ 2 ] [ - 20 : ] . hex ( )
#ts = ContractRegistry.get_address(t.address)
ts = CICRegistry . get_address ( self . chain_spec , t . address ( ) )
logg . info ( ' add token transfer {} value {} from {} to {} ' . format (
ts . symbol ( ) ,
token_value ,
token_sender ,
token_recipient ,
)
)
db . add_transaction (
session ,
r . transactionHash . hex ( ) ,
r . blockNumber ,
r . transactionIndex ,
w3 . toChecksumAddress ( token_sender ) ,
w3 . toChecksumAddress ( token_recipient ) ,
t . address ( ) ,
t . address ( ) ,
token_value ,
token_value ,
r . status == 1 ,
b . timestamp ,
)
session . flush ( )
# TODO: simplify/ split up and/or comment, function is too long
def __process_convert ( self , w3 , session , t , r , l , b ) :
logg . warning ( ' conversions are deactivated ' )
return
# token_source = l.topics[2][-20:].hex()
# token_source = w3.toChecksumAddress(token_source)
# token_destination = l.topics[3][-20:].hex()
# token_destination = w3.toChecksumAddress(token_destination)
# data_noox = l.data[2:]
# d = data_noox[:64]
# token_from_value = int(d, 16)
# d = data_noox[64:128]
# token_to_value = int(d, 16)
# token_trader = '0x' + data_noox[192-40:]
#
# #ts = ContractRegistry.get_address(token_source)
# ts = CICRegistry.get_address(CICRegistry.bancor_chain_spec, t.address())
# #if ts == None:
# # ts = ContractRegistry.reserves[token_source]
# td = ContractRegistry.get_address(token_destination)
# #if td == None:
# # td = ContractRegistry.reserves[token_source]
# logg.info('add token convert {} -> {} value {} -> {} trader {}'.format(
# ts.symbol(),
# td.symbol(),
# token_from_value,
# token_to_value,
# token_trader,
# )
# )
#
# db.add_transaction(
# session,
# r.transactionHash.hex(),
# r.blockNumber,
# r.transactionIndex,
# w3.toChecksumAddress(token_trader),
# w3.toChecksumAddress(token_trader),
# token_source,
# token_destination,
# r.status == 1,
# b.timestamp,
# )
# session.flush()
def check_token ( self , address ) :
t = None
try :
t = CICRegistry . get_address ( CICRegistry . default_chain_spec , address )
return t
except UnknownContractError :
logg . debug ( ' contract {} not in registry ' . format ( address ) )
# If nothing was returned, we look up the token in the declarator
for trust in self . trusts :
logg . debug ( ' look up declaration for contract {} with trust {} ' . format ( address , trust ) )
fn = self . declarator . function ( ' declaration ' )
# TODO: cache trust in LRUcache
declaration_array = fn ( trust , address ) . call ( )
try :
declaration = to_token_declaration ( trust , address , declaration_array , [ rubberstamp ] )
logg . debug ( ' found declaration for token {} from trust address {} ' . format ( address , trust ) )
except UnknownDeclarationError :
continue
try :
c = w3 . eth . contract ( abi = CICRegistry . abi ( ' ERC20 ' ) , address = address )
t = CICRegistry . add_token ( self . chain_spec , c )
break
except ValueError :
logg . error ( ' declaration for {} validates as token, but location is not ERC20 compatible ' . format ( address ) )
return t
# TODO use input data instead of logs
def process ( self , w3 , session , block ) :
#self.refresh_registry(w3)
tx_count = w3 . eth . getBlockTransactionCount ( block . hash )
b = w3 . eth . getBlock ( block . hash )
for i in range ( self . tx_height , tx_count ) :
tx = w3 . eth . getTransactionByBlock ( block . hash , i )
if tx . to == None :
logg . debug ( ' block {} tx {} is contract creation tx, skipping ' . format ( block . number , i ) )
continue
if len ( w3 . eth . getCode ( tx . to ) ) == 0 :
logg . debug ( ' block {} tx {} not a contract tx, skipping ' . format ( block . number , i ) )
continue
t = self . check_token ( tx . to )
if t != None and isinstance ( t , Token ) :
r = w3 . eth . getTransactionReceipt ( tx . hash )
for l in r . logs :
logg . debug ( ' block {} tx {} {} token log {} {} ' . format ( block . number , i , tx . hash . hex ( ) , l . logIndex , l . topics [ 0 ] . hex ( ) ) )
if l . topics [ 0 ] . hex ( ) == log_topics [ ' transfer ' ] :
self . __process_tx ( w3 , session , t , r , l , b )
# TODO: cache contracts in LRUcache
elif self . convert_enabled and tx . to == CICRegistry . get_contract ( CICRegistry . default_chain_spec , ' Converter ' ) . address :
r = w3 . eth . getTransactionReceipt ( tx . hash )
for l in r . logs :
logg . info ( ' block {} tx {} {} bancornetwork log {} {} ' . format ( block . number , i , tx . hash . hex ( ) , l . logIndex , l . topics [ 0 ] . hex ( ) ) )
if l . topics [ 0 ] . hex ( ) == log_topics [ ' convert ' ] :
self . __process_convert ( w3 , session , t , r , l , b )
session . execute ( " UPDATE tx_sync SET tx = ' {} ' " . format ( tx . hash . hex ( ) ) )
session . commit ( )
self . tx_height + = 1
def __get_next_retry ( self , backoff = False ) :
return 1
def loop ( self ) :
logg . info ( ' starting at block {} tx index {} ' . format ( self . block_height , self . tx_height ) )
self . state = RunStateEnum . RUN
while self . state == RunStateEnum . RUN :
( provider , w3 ) = web3_constructor ( )
session = SessionBase . create_session ( )
try :
block = w3 . eth . getBlock ( self . block_height )
self . process ( w3 , session , block )
self . block_height + = 1
self . tx_height = 0
except BlockNotFound as e :
logg . debug ( ' no block {} yet, zZzZ... ' . format ( self . block_height ) )
time . sleep ( self . __get_next_retry ( ) )
except ConnectionClosedError as e :
logg . info ( ' connection gone, retrying ' )
time . sleep ( self . __get_next_retry ( True ) )
except OSError as e :
logg . error ( ' cannot connect {} ' . format ( e ) )
time . sleep ( self . __get_next_retry ( True ) )
except Exception as e :
session . close ( )
raise ( e )
session . close ( )
def load ( self , w3 ) :
session = SessionBase . create_session ( )
r = session . execute ( ' SELECT tx FROM tx_sync ' ) . first ( )
if r != None :
if r [ 0 ] == ' 0x { 0:0 {1} X} ' . format ( 0 , 64 ) :
logg . debug ( ' last tx was zero-address, starting from scratch ' )
return
t = w3 . eth . getTransaction ( r [ 0 ] )
self . block_height = t . blockNumber
self . tx_height = t . transactionIndex + 1
c = w3 . eth . getBlockTransactionCount ( t . blockHash . hex ( ) )
logg . debug ( ' last tx processed {} index {} (max index {} ) ' . format ( t . blockNumber , t . transactionIndex , c - 1 ) )
if c == self . tx_height :
self . block_height + = 1
self . tx_height = 0
session . close ( )
( provider , w3 ) = web3_constructor ( )
2021-03-02 18:33:13 +01:00
trust = config . get ( ' CIC_TRUST_ADDRESS ' , " " ) . split ( " , " )
2021-02-18 06:04:30 +01:00
chain_spec = args . i
try :
w3 . eth . chainId
except Exception as e :
logg . exception ( e )
sys . stderr . write ( ' cannot connect to evm node \n ' )
sys . exit ( 1 )
def main ( ) :
chain_spec = ChainSpec . from_chain_str ( config . get ( ' CIC_CHAIN_SPEC ' ) )
CICRegistry . init ( w3 , config . get ( ' CIC_REGISTRY_ADDRESS ' ) , chain_spec )
CICRegistry . add_path ( config . get ( ' ETH_ABI_DIR ' ) )
chain_registry = ChainRegistry ( chain_spec )
CICRegistry . add_chain_registry ( chain_registry )
t = Tracker ( chain_spec , trust )
t . load ( w3 )
t . loop ( )
if __name__ == ' __main__ ' :
main ( )