moonworm is a code generation tool which makes it easy
for anyone to interact with smart contracts on an EVM-based blockchain.
It is inspired by abigen
and generates Python bindings to smart contracts when provided with their ABIs as inputs.
More importantly, moonworm also generates fully-featured command-line interfaces to those smart contracts,
which is extremely useful when running live operations against deployed contracts. It is also helpful
in creating bots that interact with deployed contracts.
moonworm is capable of generating Python bindings and CLIs which use any of the following libraries
to interact with EVM-based blockchains:
defwatch_contract(web3:Web3,state_provider:EthereumStateProvider,contract_address:ChecksumAddress,contract_abi:List[Dict[str,Any]],num_confirmations:int=10,sleep_time:float=1,start_block:Optional[int]=None,end_block:Optional[int]=None,min_blocks_batch:int=100,max_blocks_batch:int=5000,batch_size_update_threshold:int=100,only_events:bool=False,outfile:Optional[str]=None,)->None:""" Watches a contract for events and calls. """def_crawl_events(event_abi,from_block:int,to_block:int,batch_size:int)->Tuple[List[Dict[str,Any]],int]:""" Crawls events from the given block range. reduces the batch_size if response is failing. increases the batch_size if response is successful. """events=[]current_from_block=from_blockwhilecurrent_from_block<=to_block:current_to_block=min(current_from_block+batch_size,to_block)try:events_chunk=_fetch_events_chunk(web3,event_abi,current_from_block,current_to_block,[contract_address],)events.extend(events_chunk)current_from_block=current_to_block+1iflen(events)<=batch_size_update_threshold:batch_size=min(batch_size*2,max_blocks_batch)exceptExceptionase:ifbatch_size<=min_blocks_batch:raiseetime.sleep(0.1)batch_size=max(batch_size//2,min_blocks_batch)returnevents,batch_sizecurrent_batch_size=min_blocks_batchstate=MockState()crawler=FunctionCallCrawler(state,state_provider,contract_abi,[web3.toChecksumAddress(contract_address)],)event_abis=[itemforitemincontract_abiifitem["type"]=="event"]ifstart_blockisNone:current_block=web3.eth.blockNumber-num_confirmations*2else:current_block=start_blockprogress_bar=tqdm(unit=" blocks")progress_bar.set_description(f"Current block {current_block}")ofp=NoneifoutfileisnotNone:ofp=open(outfile,"a")try:whileend_blockisNoneorcurrent_block<=end_block:time.sleep(sleep_time)until_block=min(web3.eth.blockNumber-num_confirmations,current_block+current_batch_size,)ifend_blockisnotNone:until_block=min(until_block,end_block)ifuntil_block<current_block:sleep_time*=2continuesleep_time/=2ifnotonly_events:crawler.crawl(current_block,until_block)ifstate.state:print("Got transaction calls:")forcallinstate.state:pp.pprint(call,width=200,indent=4)ifofpisnotNone:print(json.dumps(asdict(call)),file=ofp)ofp.flush()state.flush()forevent_abiinevent_abis:all_events,new_batch_size=_crawl_events(event_abi,current_block,until_block,current_batch_size)ifonly_events:# Updating batch size only in `--only-events` mode# otherwise it will start taking too much if we also crawl transactionscurrent_batch_size=new_batch_sizeforeventinall_events:print("Got event:")pp.pprint(event,width=200,indent=4)ifofpisnotNone:print(json.dumps(event),file=ofp)ofp.flush()progress_bar.set_description(f"Current block {until_block}, Already watching for")progress_bar.update(until_block-current_block+1)current_block=until_block+1finally:ifofpisnotNone:ofp.close()
State providers: Accessing blockchain state
watch_contract uses EthereumStateProvider
objects to crawl blockchain state. Any object inheriting from the EthereumStateProvider base class
will suffice.
EthereumStateProvider
Abstract class for Ethereum state provider.
If you want to use a different state provider, you can implement this class.
Source code in moonworm/crawler/ethereum_state_provider.py
classEthereumStateProvider(ABC):""" Abstract class for Ethereum state provider. If you want to use a different state provider, you can implement this class. """@abstractmethoddefget_last_block_number(self)->int:""" Returns the last block number. """pass@abstractmethoddefget_block_timestamp(self,block_number:int)->int:""" Returns the timestamp of the block with the given block number. """pass@abstractmethoddefget_transactions_to_address(self,address,block_number:int)->List[Dict[str,Any]]:""" Returns all transactions to the given address in the given block number. """pass
classWeb3StateProvider(EthereumStateProvider):""" Implementation of EthereumStateProvider with web3. """def__init__(self,w3:Web3):self.w3=w3self.blocks_cache={}defget_transaction_reciept(self,transaction_hash:str)->Dict[str,Any]:returnself.w3.eth.get_transaction_receipt(transaction_hash)defget_last_block_number(self)->int:returnself.w3.eth.block_numberdef_get_block(self,block_number:int)->Dict[str,Any]:ifblock_numberinself.blocks_cache:returnself.blocks_cache[block_number]block=self.w3.eth.getBlock(block_number,full_transactions=True)# clear cache if it grows too largeiflen(self.blocks_cache)>50:self.blocks_cache={}self.blocks_cache[block_number]=blockreturnblockdefget_block_timestamp(self,block_number:int)->int:block=self._get_block(block_number)returnblock["timestamp"]defget_transactions_to_address(self,address:ChecksumAddress,block_number:int)->List[Dict[str,Any]]:block=self._get_block(block_number)all_transactions=block["transactions"]return[txfortxinall_transactionsiftx.get("to")==address]
CLI: moonworm watch
To access this functionality from the moonworm command-line interface, use the moonworm watch command:
moonworm watch --help
Discovering the block at which a smart contract was deployed
deffind_deployment_block(web3_client:Web3,contract_address:ChecksumAddress,web3_interval:float,)->Optional[int]:""" Note: We will assume no selfdestruct for now. This means that, if the address does not currently contain code, we will assume it never contained code and is therefore not a smart contract address. """log_prefix=f"find_deployment_block(web3_client, contract_address={contract_address}, web3_interval={web3_interval}) -- "logger.info(f"{log_prefix}Function invoked")config={CONFIG_KEY_WEB3_INTERVAL:web3_interval}max_block=int(web3_client.eth.block_number)min_block=0middle_block=int((min_block+max_block)/2)was_deployed_at_max_block=was_deployed_at_block(web3_client,contract_address,max_block,config=config)ifnotwas_deployed_at_max_block:logger.warn(f"{log_prefix}Address is not a smart contract")returnNonewas_deployed:Dict[int,bool]={max_block:was_deployed_at_max_block,min_block:was_deployed_at_block(web3_client,contract_address,min_block,config=config),middle_block:was_deployed_at_block(web3_client,contract_address,middle_block,config=config),}whilemax_block-min_block>=2:logger.info(f"{log_prefix}Binary search -- max_block={max_block}, min_block={min_block}, middle_block={middle_block}")ifnotwas_deployed[min_block]andnotwas_deployed[middle_block]:min_block=middle_blockelse:max_block=middle_blockmiddle_block=int((min_block+max_block)/2)was_deployed[middle_block]=was_deployed_at_block(web3_client,contract_address,middle_block,config=config)ifwas_deployed[min_block]:returnmin_blockreturnmax_block
CLI: moonworm find-deployment
To access this functionality from the moonworm command-line interface, use the moonworm find-deployment command: