6

I have a raw_tx in hex format which I'm going to broadcast via eth_sendrawtransaction JSON-RPC method.

I want to check this raw_tx by decoding it on fields: gas, gas_price, nonce, value, etc.

Some years ago I already did it in JavaScript. Are there any libraries in Python which can do it?

I think it's possible to do it via this library: https://github.com/ethereum/pyrlp

But I haven't found there a structure of Ethereum tx object.

4

3 Answers 3

7

Previous answer is great and works but if you want a shorter code using only 3rd libraries, you can do this:

import rlp
from eth_typing import HexStr
from eth_utils import to_bytes
from ethereum.transactions import Transaction

def hex_to_bytes(data: str) -> bytes:
    return to_bytes(hexstr=HexStr(data))

Then, to decode:

txs = rlp.decode(hex_to_bytes("0xf8a910850684ee180082e48694a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f9200000000000000000000000000000000000000000000000000000000010366401ba0e2a4093875682ac6a1da94cdcc0a783fe61a7273d98e1ebfe77ace9cab91a120a00f553e48f3496b7329a7c0008b3531dd29490c517ad28b0e6c1fba03b79a1dee"), Transaction)
txs.to_dict()

Output:

{'nonce': 16,
 'gasprice': 28000000000,
 'startgas': 58502,
 'to': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
 'value': 0,
 'data': '0xa9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f920000000000000000000000000000000000000000000000000000000001036640',
 'v': 27,
 'r': 102512530336280383259877025531328105975098228108468048464966947520424071766304,
 's': 6935304604265909542304656854134707256369339281795067289224235008159636921838,
 'sender': '0xd8ce57b469962b6ea944d28b741312fb7e78cfaf',
 'hash': '0xb808400bd5a1dd9c37960c515d2493c380b829c5a592e499ed0d5d9913a6a446'}
6

Code:

from dataclasses import asdict, dataclass
from pprint import pprint
from typing import Optional

import rlp
from eth_typing import HexStr
from eth_utils import keccak, to_bytes
from rlp.sedes import Binary, big_endian_int, binary
from web3 import Web3
from web3.auto import w3


class Transaction(rlp.Serializable):
    fields = [
        ("nonce", big_endian_int),
        ("gas_price", big_endian_int),
        ("gas", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("v", big_endian_int),
        ("r", big_endian_int),
        ("s", big_endian_int),
    ]


@dataclass
class DecodedTx:
    hash_tx: str
    from_: str
    to: Optional[str]
    nonce: int
    gas: int
    gas_price: int
    value: int
    data: str
    chain_id: int
    r: str
    s: str
    v: int


def hex_to_bytes(data: str) -> bytes:
    return to_bytes(hexstr=HexStr(data))


def decode_raw_tx(raw_tx: str):
    tx = rlp.decode(hex_to_bytes(raw_tx), Transaction)
    hash_tx = Web3.toHex(keccak(hex_to_bytes(raw_tx)))
    from_ = w3.eth.account.recover_transaction(raw_tx)
    to = w3.toChecksumAddress(tx.to) if tx.to else None
    data = w3.toHex(tx.data)
    r = hex(tx.r)
    s = hex(tx.s)
    chain_id = (tx.v - 35) // 2 if tx.v % 2 else (tx.v - 36) // 2
    return DecodedTx(hash_tx, from_, to, tx.nonce, tx.gas, tx.gas_price, tx.value, data, chain_id, r, s, tx.v)


def main():
    raw_tx = "0xf8a910850684ee180082e48694a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f9200000000000000000000000000000000000000000000000000000000010366401ba0e2a4093875682ac6a1da94cdcc0a783fe61a7273d98e1ebfe77ace9cab91a120a00f553e48f3496b7329a7c0008b3531dd29490c517ad28b0e6c1fba03b79a1dee"  # noqa
    res = decode_raw_tx(raw_tx)
    pprint(asdict(res))


if __name__ == "__main__":
    main()

Outputs:

{'chain_id': -4,
 'data': '0xa9059cbb000000000000000000000000b8b59a7bc828e6074a4dd00fa422ee6b92703f920000000000000000000000000000000000000000000000000000000001036640',
 'from_': '0xD8cE57B469962b6Ea944d28b741312Fb7E78cfaF',
 'gas': 58502,
 'gas_price': 28000000000,
 'hash_tx': '0xb808400bd5a1dd9c37960c515d2493c380b829c5a592e499ed0d5d9913a6a446',
 'nonce': 16,
 'r': '0xe2a4093875682ac6a1da94cdcc0a783fe61a7273d98e1ebfe77ace9cab91a120',
 's': '0xf553e48f3496b7329a7c0008b3531dd29490c517ad28b0e6c1fba03b79a1dee',
 'to': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
 'v': 27,
 'value': 0}

0

The proposed solution by noleto relies on using pyethereum, which has been deprecated and can lead to dependency version conflicts (you should use an old version of rlp). Moreover, answers provided so far do not support for EIP-1559 transactions. The most effective solution I've come across is based on the code found in the eth-account lib.

from eth_account._utils.typed_transactions import TypedTransaction
from eth_account._utils.legacy_transactions import Transaction, vrs_from
from eth_account._utils.signing import hash_of_signed_transaction
from eth_account import Account
from hexbytes import HexBytes

def decode(raw_tx):
    txn_bytes = HexBytes(raw_tx)
    if len(txn_bytes) > 0 and txn_bytes[0] <= 0x7F:
        # We are dealing with a typed transaction.
        tx_type = 2
        tx = TypedTransaction.from_bytes(txn_bytes)
        msg_hash = tx.hash()
        vrs = tx.vrs()
    else:
        # We are dealing with a legacy transaction.
        tx_type = 0
        tx = Transaction.from_bytes(txn_bytes)
        msg_hash = hash_of_signed_transaction(tx)
        vrs = vrs_from(tx)
    
    # extracting sender address
    sender = Account._recover_hash(msg_hash, vrs=vrs)

    # adding sender to result and cleaning
    res = tx.as_dict()
    res["from"] = sender
    res["to"] = res["to"].hex()
    res["data"] = res["data"].hex()
    res["type"] = res.get("type", tx_type)

    return res

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.