Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# REQUIRED:

# WEB3 NODES:
# Proper nodes are required to run ethtx.
# Values are populated from the environment by treating the environment variable's value as a JSON-encoded string.
# EXAMPLE: WEB3_NODES='{"mainnet": {"hook": "https://geth", "poa": false}, "rinkeby": {"hook": "https://eth-rinkeby", "poa": true}}'
# EthTx supports multiple nodes, if one is unavailable, it will use others. You only need to specify them with a comma.
WEB3_NODES=

# ETHERSCAN:
# Etherscan API is used to get contract source code, required for decoding process
# You can get free key here https://etherscan.io/apis
ETHERSCAN_API_KEY=


# OPTIONAL:

# DEFAULT_CHAIN:
# Default chain to use when no chain is specified.
DEFAULT_CHAIN=mainnet

# CACHE_SIZE:
# lru_cache size.
CACHE_SIZE=128

# MONGO_CONNECTION_STRING:
# Those represent data required for connecting to mongoDB. It's used for caching semantics
# used in decoding process. But, it's not neccessary for running, If you don't want to use permanent
# db or setup mongo, leave those values, mongomock package is used to simulate in-memory mongo.
MONGO_CONNECTION_STRING=mongomock://localhost/ethtx

# ETHERSCAN_URLS:
# URLs for etherscan APIs.
# Values are populated from the environment by treating the environment variable's value as a JSON-encoded string.
ETHERSCAN_URLS='{"mainnet": "https://api.etherscan.io/api", "rinkeby": "https://api-rinkeby.etherscan.io/api", "goerli": "https://api-goerli.etherscan.io/api"}'


4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ pymongo = ">=3.12.0"
dnspython = ">=2.2.0"
mongoengine = "==0.23.1"
mongomock = "==3.23.0"
web3 = "==5.26.0"
eth-account = "==0.5.6"
web3 = "==5.28.0"
eth-account = "==0.5.7"
eth-utils = "==1.10.0"
eth-rlp = "==0.2.1"
pydantic = ">=1.9.0"
Expand Down
104 changes: 69 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,58 @@ The package needs a few external resources, defined in `EthTxConfig` object:
the `debug` option ON
2. **Etherscan API key** - required to get the source code and ABI for smart contracts used in transaction
3. (Optional) **MongoDB database** - required to store smart contracts' ABI and semantics used in the decoding process.
If you don't want to setup permanent database, you can enter `mongomock://localhost`, then in-memory mongo will be
If you don't want to setup permanent database, you can enter `mongomock://localhost/ethtx`, then in-memory mongo will be
set up that discards all data with every run.
4. Copy `.env_sample` to `.env` and fill required field according to description
```dotenv
# REQUIRED:

# WEB3 NODES:
# Proper nodes are required to run ethtx.
# Values are populated from the environment by treating the environment variable's value as a JSON-encoded string.
# EXAMPLE: WEB3_NODES='{"mainnet": {"hook": "https://geth", "poa": false}, "rinkeby": {"hook": "https://eth-rinkeby", "poa": true}}'
# EthTx supports multiple nodes, if one is unavailable, it will use others. You only need to specify them with a comma.
WEB3_NODES=

# ETHERSCAN:
# Etherscan API is used to get contract source code, required for decoding process
# You can get free key here https://etherscan.io/apis
ETHERSCAN_API_KEY=


# OPTIONAL:

# DEFAULT_CHAIN:
# Default chain to use when no chain is specified.
DEFAULT_CHAIN=mainnet

# CACHE_SIZE:
# lru_cache size.
CACHE_SIZE=128

# MONGO_CONNECTION_STRING:
# Those represent data required for connecting to mongoDB. It's used for caching semantics
# used in decoding process. But, it's not neccessary for running, If you don't want to use permanent
# db or setup mongo, leave those values, mongomock package is used to simulate in-memory mongo.
MONGO_CONNECTION_STRING=mongomock://localhost/ethtx

# ETHERSCAN_URLS:
# URLs for etherscan APIs.
# Values are populated from the environment by treating the environment variable's value as a JSON-encoded string.
ETHERSCAN_URLS='{"mainnet": "https://api.etherscan.io/api", "rinkeby": "https://api-rinkeby.etherscan.io/api", "goerli": "https://api-goerli.etherscan.io/api"}'
```

## Getting started

```python
from dotenv import load_dotenv

load_dotenv("<PATH_TO_ENV_FILE>")

from ethtx import EthTx, EthTxConfig
from ethtx.models.decoded_model import DecodedTransaction

ethtx_config = EthTxConfig(
mongo_connection_string="mongomock://localhost/ethtx", ##MongoDB connection string,
etherscan_api_key="", ##Etherscan API key,
web3nodes={
"mainnet": {
"hook": "_Geth_archive_node_URL_", # multiple nodes supported, separate them with comma
"poa": _POA_chain_indicator_ # represented by bool value
}
},
default_chain="mainnet",
etherscan_urls={"mainnet": "https://api.etherscan.io/api", },
)

ethtx = EthTx.initialize(ethtx_config)
ethtx = EthTx.initialize(EthTxConfig)
transaction: DecodedTransaction = ethtx.decoders.decode_transaction(
'0x50051e0a6f216ab9484c2080001c7e12d5138250acee1f4b7c725b8fb6bb922d')
```
Expand All @@ -72,7 +101,6 @@ EthTx most important functions:
1. Raw node data access:

```python
ethtx = EthTx.initialize(ethtx_config)
web3provider = ethtx.providers.web3provider

from ethtx.models.w3_model import W3Transaction, W3Block, W3Receipt, W3CallTree
Expand All @@ -96,25 +124,25 @@ from ethtx.models.decoded_model import (
from ethtx.models.objects_model import Transaction, Event, Block, Call

# read the raw transaction from the node
transaction: Transaction = web3provider.get_full_transaction(
'0x50051e0a6f216ab9484c2080001c7e12d5138250acee1f4b7c725b8fb6bb922d')
transaction = Transaction.from_raw(
w3transaction=w3transaction, w3receipt=w3receipt, w3calltree=w3calls
)

# get proxies used in the transaction
proxies = ethtx.decoders.get_proxies(transaction.root_call, 'mainnet')
proxies = ethtx.decoders.get_proxies(transaction.root_call, "mainnet")

block: Block = Block.from_raw(
w3block=web3provider.get_block(
transaction.metadata.block_number
),
chain_id='mainnet',
w3block=web3provider.get_block(transaction.metadata.block_number),
chain_id="mainnet",
)

# decode transaction components
abi_decoded_events: List[Event] = ethtx.decoders.abi_decoder.decode_events(
transaction.events, block.metadata, transaction.metadata
)
abi_decoded_calls: DecodedCall = ethtx.decoders.abi_decoder.decode_calls(transaction.root_call, block.metadata,
transaction.metadata, proxies)
abi_decoded_calls: DecodedCall = ethtx.decoders.abi_decoder.decode_calls(
transaction.root_call, block.metadata, transaction.metadata, proxies
)
abi_decoded_transfers: List[
DecodedTransfer
] = ethtx.decoders.abi_decoder.decode_transfers(abi_decoded_calls, abi_decoded_events)
Expand All @@ -124,13 +152,15 @@ abi_decoded_balances: List[DecodedBalance] = ethtx.decoders.abi_decoder.decode_b

# decode a single event
raw_event: Event = transaction.events[3]
abi_decoded_event: DecodedEvent = ethtx.decoders.abi_decoder.decode_event(raw_event, block.metadata,
transaction.metadata)
abi_decoded_event: DecodedEvent = ethtx.decoders.abi_decoder.decode_event(
raw_event, block.metadata, transaction.metadata
)

# decode a single call
raw_call: Call = transaction.root_call.subcalls[0]
abi_decoded_call: DecodedCall = ethtx.decoders.abi_decoder.decode_call(raw_call, block.metadata, transaction.metadata,
proxies)
abi_decoded_call: DecodedCall = ethtx.decoders.abi_decoder.decode_call(
raw_call, block.metadata, transaction.metadata, proxies
)
```

3. Semantic decoding:
Expand All @@ -141,14 +171,16 @@ from ethtx.models.decoded_model import DecodedTransactionMetadata
# semantically decode transaction components
decoded_metadata: DecodedTransactionMetadata = (
ethtx.decoders.semantic_decoder.decode_metadata(
block.metadata, transaction.metadata, 'mainnet'
block.metadata, transaction.metadata, "mainnet"
)
)
decoded_events: List[DecodedEvent] = ethtx.decoders.semantic_decoder.decode_events(
abi_decoded_events, decoded_metadata, proxies
)

decoded_calls: Call = ethtx.decoders.semantic_decoder.decode_calls(abi_decoded_calls, decoded_metadata, proxies)
decoded_calls: Call = ethtx.decoders.semantic_decoder.decode_calls(
abi_decoded_calls, decoded_metadata, proxies
)
decoded_transfers: List[
DecodedTransfer
] = ethtx.decoders.semantic_decoder.decode_transfers(
Expand All @@ -161,9 +193,11 @@ decoded_balances: List[
)

# semantically decode a single event
decoded_event: DecodedEvent = ethtx.decoders.semantic_decoder.decode_event(abi_decoded_events[0], decoded_metadata,
proxies)
decoded_event: DecodedEvent = ethtx.decoders.semantic_decoder.decode_event(
abi_decoded_events[0], decoded_metadata, proxies
)
# semantically decode a single call
decoded_call: Call = ethtx.decoders.semantic_decoder.decode_call(abi_decoded_calls.subcalls[0],
decoded_metadata, proxies)
decoded_call: Call = ethtx.decoders.semantic_decoder.decode_call(
abi_decoded_calls.subcalls[0], decoded_metadata, proxies
)
```
3 changes: 2 additions & 1 deletion ethtx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from .ethtx import EthTx, EthTxConfig
from .core.config import EthTxConfig
from .ethtx import EthTx
Empty file added ethtx/core/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions ethtx/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pydantic import BaseSettings


class Settings(BaseSettings):
DEFAULT_CHAIN: str = "mainnet"
CACHE_SIZE: int = 128

MONGO_CONNECTION_STRING: str = "mongomock://localhost/ethtx"

WEB3_NODES: dict

ETHERSCAN_API_KEY: str
ETHERSCAN_URLS: dict = {
"mainnet": "https://api.etherscan.io/api",
"goerli": "https://api-goerli.etherscan.io/api",
"rinkeby": "https://api-rinkeby.etherscan.io/api",
}

class Config:
case_sensitive = True
env_file = "../../.env"


EthTxConfig = Settings()
2 changes: 1 addition & 1 deletion ethtx/decoders/abi/calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def decode_call(
call_type=call.call_type,
from_address=AddressInfo(address=call.from_address, name=from_name),
to_address=AddressInfo(address=call.to_address, name=to_name),
value=call.call_value / 10 ** 18,
value=call.call_value / 10**18,
function_signature=function_signature,
function_name=function_name,
arguments=function_input,
Expand Down
2 changes: 1 addition & 1 deletion ethtx/decoders/abi/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def _transfers_calls(decoded_call):
) = self._repository.get_token_data(
event.chain_id, event.contract.address, proxies
)
value = event.parameters[2].value / 10 ** token_decimals
value = event.parameters[2].value / 10**token_decimals
transfers.append(
DecodedTransfer(
from_address=AddressInfo(
Expand Down
1 change: 0 additions & 1 deletion ethtx/decoders/semantic/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
log = logging.getLogger(__name__)



def get_badge(address, sender, receiver):
sender_address = sender.address if isinstance(sender, AddressInfo) else sender
receiver_address = (
Expand Down
2 changes: 1 addition & 1 deletion ethtx/decoders/semantic/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def decode(
block_number=block_metadata.block_number,
block_hash=block_metadata.block_hash,
timestamp=block_metadata.timestamp,
gas_price=tx_metadata.gas_price / 10 ** 9,
gas_price=tx_metadata.gas_price / 10**9,
sender=AddressInfo(
address=tx_metadata.from_address,
name=self.repository.get_address_label(
Expand Down
37 changes: 8 additions & 29 deletions ethtx/ethtx.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from mongoengine import connect
from pymongo import MongoClient

from .core.config import Settings
from .decoders.abi.decoder import ABIDecoder
from .decoders.decoder_service import DecoderService
from .decoders.semantic.decoder import SemanticDecoder
Expand All @@ -29,28 +30,6 @@
from .utils.validators import assert_tx_hash


class EthTxConfig:
mongo_connection_string: str
etherscan_api_key: str
web3nodes: Dict[str, dict]
etherscan_urls: Dict[str, str]
default_chain: str

def __init__(
self,
mongo_connection_string: str,
web3nodes: Dict[str, dict],
etherscan_api_key: str,
etherscan_urls: Dict[str, str],
default_chain: str = "mainnet",
):
self.mongo_connection_string = mongo_connection_string
self.etherscan_api_key = etherscan_api_key
self.web3nodes = web3nodes
self.default_chain = default_chain
self.etherscan_urls = etherscan_urls


class EthTxDecoders:
semantic_decoder: SemanticDecoder
abi_decoder: ABIDecoder
Expand Down Expand Up @@ -117,23 +96,23 @@ def __init__(
)

@staticmethod
def initialize(config: EthTxConfig):
mongo_client: MongoClient = connect(host=config.mongo_connection_string)
def initialize(config: Settings):
mongo_client: MongoClient = connect(host=config.MONGO_CONNECTION_STRING)
repository = MongoSemanticsDatabase(db=mongo_client.get_database())

web3provider = Web3Provider(
nodes=config.web3nodes, default_chain=config.default_chain
nodes=config.WEB3_NODES, default_chain=config.DEFAULT_CHAIN
)
etherscan_provider = EtherscanProvider(
api_key=config.etherscan_api_key,
nodes=config.etherscan_urls,
default_chain_id=config.default_chain,
api_key=config.ETHERSCAN_API_KEY,
nodes=config.ETHERSCAN_URLS,
default_chain_id=config.DEFAULT_CHAIN,
)

ens_provider = ENSProvider

return EthTx(
config.default_chain,
config.DEFAULT_CHAIN,
repository,
web3provider,
etherscan_provider,
Expand Down
7 changes: 4 additions & 3 deletions ethtx/models/semantics_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import List, Dict, Optional, TYPE_CHECKING

from ethtx.models.base_model import BaseModel

if TYPE_CHECKING:
from ethtx.providers.semantic_providers import ISemanticsDatabase

Expand Down Expand Up @@ -85,9 +86,10 @@ class AddressSemantics(BaseModel):
class Config:
allow_mutation = True


@staticmethod
def from_mongo_record(raw_address_semantics: Dict, database: 'ISemanticsDatabase') -> 'AddressSemantics':
def from_mongo_record(
raw_address_semantics: Dict, database: "ISemanticsDatabase"
) -> "AddressSemantics":

ZERO_HASH = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"

Expand Down Expand Up @@ -182,7 +184,6 @@ def decode_parameter(_parameter):
chain_id = raw_address_semantics.get("chain_id")
name = raw_address_semantics.get("name", address)


address_semantics = AddressSemantics(
chain_id=chain_id,
address=address,
Expand Down
Loading