Reliable and debug-friendly Ethereum client
- Be a thin, debuggable and battle tested wrapper on top of
go-ethereum - Decode all transaction inputs/outputs/logs for all ABIs you are working with, automatically
- Simple synchronous API
- Do not handle
nonceson the client side, trust the server - Do not wrap
bindgenerated contracts, small set of additional debug API - Resilient: should execute transactions even if there is a gas spike or an RPC outage (failover)
- Well tested: should provide a suite of e2e tests that can be run on testnets to check integration
Check examples folder
Lib is providing a small amount of helpers for decoding handling that you can use with vanilla go-ethereum generated wrappers
// Decode waits for transaction and decode all the data/errors
Decode(tx *types.Transaction, txErr error) (*DecodedTransaction, error)
// NewTXOpts returns a new sequential transaction options wrapper,
// sets opts.GasPrice and opts.GasLimit from seth.toml or override with options
NewTXOpts(o ...TransactOpt) *bind.TransactOpts
// NewCallOpts returns a new call options wrapper
NewCallOpts(o ...CallOpt) *bind.CallOpts
By default, we are using the root key 0, but you can also use keys from keyfile.toml
// NewCallKeyOpts returns a new sequential call options wrapper from the key N
NewCallKeyOpts(keyNum int, o ...CallOpt) *bind.CallOpts
// NewTXKeyOpts returns a new transaction options wrapper called from the key N
NewTXKeyOpts(keyNum int, o ...TransactOpt) *bind.TransactOpts
Start Geth in a separate terminal, then run the examples
make GethSync
cd examples
go test -v
We are using nix
Enter the shell
nix develop
We have go-ethereum and foundry tools inside nix shell
make build
To run tests on a local network, first start it
make AnvilSync
Or use latest Geth
make GethSync
You can use default hardhat key ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 to run tests
Run the decode tests
make network=Anvil root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test
make network=Geth root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test
Check other params in seth.toml, select any network and use your key for testnets
User facing API tests are here
make network=Anvil root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_api
make network=Geth root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_api
CLI tests
make network=Anvil root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_cli
make network=Geth root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_cli
Tracing tests
make network=Anvil root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_trace
make network=Geth root_private_key=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 test_trace
Some crucial data is stored in env vars, create .envrc and use source .envrc, or use direnv
export SETH_LOG_LEVEL=info # global logger level
export SETH_CONFIG_PATH=seth.toml # path to the toml config
export SETH_KEYFILE_PATH=keyfile.toml # keyfile path for using multiple keys
export SETH_NETWORK=Geth # selected network
export SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # root private key
export SETH_ONE_PASS_VAULT=712jkhjdyf71289hdjfs7d # id of 1password vault in which we store secrets
alias seth="SETH_CONFIG_PATH=seth.toml go run cmd/seth/seth.go" # useful alias for keyfile CLI
Alternatively if you don't have a network defined in the TOML you can still use the CLI by providing these 2 key env vars:
export SETH_URL=https://rpc.fuji.testnet.anyswap.exchange
export SETH_CHAIN_ID=43113
go run cmd/seth/seth.go ... # your command
In that case you should still pass network name with -n flag, especially when using CLI with 1password as network name is used when generating item name.
If SETH_KEYFILE_PATH is not set then client will create X ephemeral keys (60 by default, configurable) and won't return any funds.
Use SETH_KEYFILE_PATH for testnets/mainnets and ephemeral mode only when testing against simulated network.
Set up your ABI directory (relative to seth.toml)
abi_dir = "contracts/abi"
Setup your BIN directory (relative to seth.toml)
bin_dir = "contracts/bin"
Decide whether you want to read keyfile or use ephemeral keys. In the first case you have two options:
- read it from the filesystem
# If empty Seth will not try to load any keyfiles. You can either set it to 'file' to load keyfiles from
# a file (providing path to it in 'keyfile_path') or to 'base64_env' to load it from Base64-ed environment variable
# 'SETH_KEYFILE_BASE64'
keyfile_source = "file"
# If keyfile_source is set to 'file' this should be a path to a file with keyfiles
keyfile_path = "keyfile.toml"- read it from environment variable
SETH_KEYFILE_BASE64(keyfile needs to be base64-ed)
keyfile_source = "base64_env"If you want to use ephemeral keys, you can set the number of keys to be generated:
# Set number of ephemeral keys to be generated (0 for no ephemeral keys). Each key will receive a proportion of native tokens from root private key's balance with the value equal to `(root_balance / ephemeral_keys_number) - transfer_fee * ephemeral_keys_number`. Using ephemeral keys together with keyfile will result in an error.
ephemeral_addresses_number = 10You cannot use both keyfile and ephemeral keys at the same time. Trying to do so will cause configuration error.
You can enable auto-tracing for all transactions meeting configured level, which means that every time you use Decode() we will decode the transaction and also trace all calls made within the transaction, together with all inputs, outputs, logs and events. Three tracing levels are available:
all- trace all transactionsreverted- trace only reverted transactions (that's default setting used if you don't settracing_level)none- don't trace any transactions Example:
tracing_level = "reverted"
Additionally, you can decide where tracing/decoding data goes to. There are three options:
console- we will print all tracing data to the consolejson- we will save tracing data for each transaction to a JSON filedot- we will save tracing data for each transaction to a DOT file (graph)
trace_outputs = ["console", "json", "dot"]
For info on viewing DOT files please check the DOT graphs section below.
Example:
These two options should be used with care, when tracing_level is set to all as they might generate a lot of data.
If you want to check if the RPC is healthy on start, you can enable it with:
check_rpc_health_on_start = false
It will execute a simple check of transferring 10k wei from root key to root key and check if the transaction was successful.
You can add more networks like this:
[[Networks]]
name = "Fuji"
transaction_timeout = "30s"
# gas limit should be explicitly set only if you are connecting to a node that's incapable of estimating gas limit itself (should only happen for very old versions)
# gas_limit = 9_000_000
# hardcoded gas limit for sending funds that will be used if estimation of gas limit fails
transfer_gas_fee = 21_000
# legacy transactions
gas_price = 1_000_000_000
# EIP-1559 transactions
eip_1559_dynamic_fees = true
gas_fee_cap = 25_000_000_000
gas_tip_cap = 1_800_000_000
urls_secret = ["..."]
# if set to true we will dynamically estimate gas for every transaction (explained in more detail below)
gas_price_estimation_enabled = true
# how many last blocks to use, when estimating gas for a transaction
gas_price_estimation_blocks = 1000
# priority of the transaction, can be "fast", "standard" or "slow" (the higher the priority, the higher adjustment factor and buffer will be used for gas estimation) [default: "standard"]
gas_price_estimation_tx_priority = "slow"
If you don't we will use the default settings for Default network.
ChainID is not needed, as it's fetched from the node.
If you want to save addresses of deployed contracts, you can enable it with:
save_deployed_contracts_map = true
If you want to re-use previously deployed contracts you can indicate file name in seth.toml:
contract_map_file = "deployed_contracts_mumbai.toml"
Both features only work for live networks. Otherwise, they are ignored, and nothing is saved/read from for simulated networks.
You can either define the network you want to interact with in your TOML config and then refer it in the CLI command, or you can pass all network parameters via env vars. Most of the examples below show how to use the former approach.
To use multiple keys in your tests you can create a keyfile.toml using CLI
Set up the alias, see .envrc configuration above
export SETH_LOG_LEVEL=info # global logger level
alias seth="SETH_CONFIG_PATH=seth.toml go run cmd/seth/seth.go"
When you run any of the commands from keys namespace by default Seth will try to interact with 1password. To do that you need to meet a couple of requirements:
Now... if you are working with a vault you have access to via your desktop app, it will ask you to authenticate via the desktop app every time you need to access the vault and no further configuration is required. If it's a vault you don't have access to via the desktop app you will need to set service account token in your env vars:
export OP_SERVICE_ACCOUNT_TOKEN=...Once you have it setup you can run op vaults list to get ids of all vaults you are assigned to. The output will look more or less like this:
ID NAME
4rdre3lw7mqyz4nbrqcygdzwri Vault 1
r4qs5dh3zwofum2jse7g53cgrm Vault 2Once you have decided, which vault you want to use you need to export its id in SETH_ONE_PASS_VAULT variable. We will use Vault 1's ids in the following examples.
Create a new keyfile with 10 new accounts funded from the root key
SETH_ONE_PASS_VAULT=4rdre3lw7mqyz4nbrqcygdzwri SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 SETH_KEYFILE_PATH=keyfile_geth.toml seth -n Geth keys fund -a 10 [-b 2] [--local]
This will create a new Secure Note in 1password called GETH_KEYFILE with file attachment containing 10 new keys. Item name is always <NETWORK_NAME_>_KEYFILE. It's crucial that you always pass network name with -n flag, as it's used in item name. If it's missing we will use DEFAULT network name and it will be impossible to distinguish between keyfiles from different networks.
SETH_KEYFILE_PATH is still required, even when using 1password, because if creating the keyfile in 1password fails, we will save it to the file, so that no funds are lost.
Also remember that you don't need to define your network in TOML file as long as you pass the RPC URL with -u flag.
Run the tests, then return funds back, when needed
SETH_ONE_PASS_VAULT=4rdre3lw7mqyz4nbrqcygdzwri SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 SETH_KEYFILE_PATH=keyfile_geth.toml seth -n Geth keys return [--local]
Update the balances
SETH_ONE_PASS_VAULT=4rdre3lw7mqyz4nbrqcygdzwri SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 SETH_KEYFILE_PATH=keyfile_geth.toml seth -n Geth keys update [--local]
Remove the keyfile
SETH_ONE_PASS_VAULT=4rdre3lw7mqyz4nbrqcygdzwri SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 SETH_KEYFILE_PATH=keyfile_geth.toml seth -n Geth keys remove [--local]
If you don't want to use 1password you can still use local keyfile by providing --local flag.
In order to adjust gas price for a transaction, you can use seth gas command
seth -n Fuji gas -b 10000 -tp 0.99
This will analyze last 10k blocks and give you 25/50/75/99th/Max percentiles for base fees and tip fees
-tp 0.99 requests the 99th tip percentile across all the transaction in one block and calculates 25/50/75/99th/Max across all blocks
If you need to get some insights into network stats and create a realistic load/chaos profile with simulators (anvil as an example), you can use stats CLI command
Edit your seth.toml
[[networks]]
name = "MyCustomNetwork"
urls_secret = ["..."]
[block_stats]
rpc_requests_per_second_limit = 5
Then check the stats for the last N blocks
seth -n MyCustomNetwork stats -s -10To check stats for the interval (A, B)
seth -n MyCustomNetwork stats -s A -e BIf you don't have a network defined in the TOML you can still use the CLI by providing the RPC url via cmd arg.
Then check the stats for the last N blocks
seth -u "https://my-rpc.network.io" stats -s -10To check stats for the interval (A, B)
seth -u "https://my-rpc.network.io" stats -s A -e BResults can help you to understand if network is stable, what is avg block time, gas price, block utilization and transactions per second
# Stats
perc_95_tps = 8.0
perc_95_block_duration = '3s'
perc_95_block_gas_used = 1305450
perc_95_block_gas_limit = 15000000
perc_95_block_base_fee = 25000000000
avg_tps = 2.433333333333333
avg_block_duration = '2s'
avg_block_gas_used = 493233
avg_block_gas_limit = 15000000
avg_block_base_fee = 25000000000
# Recommended performance/chaos test parameters
duration = '2m0s'
block_gas_base_fee_initial_value = 25000000000
block_gas_base_fee_bump_percentage = '100.00% (no bump required)'
block_gas_usage_percentage = '3.28822000% gas used (no congestion)'
avg_tps = 3.0
max_tps = 8.0You can trace a single transaction using seth trace command. Example with seth alias mentioned before:
seth -u "https://my-rpc.network.io" trace -t 0x4c21294bf4c0a19de16e0fca74e1ea1687ba96c3cab64f6fca5640fb7b84df65
or if you want to use a predefined-network:
seth -n=Geth trace -t 0x4c21294bf4c0a19de16e0fca74e1ea1687ba96c3cab64f6fca5640fb7b84df65
You can trace multiple transactions at once using seth trace command for a predefined network named Geth. Example:
seth -n=Geth trace -f reverted_transactions.json
or by passing all the RPC parameter with a flag:
seth -u "https://my-rpc.network.io" trace -f reverted_transactions.json
You need to pass a file with a list of transaction hashes to trace. The file should be a JSON array of transaction hashes, like this:
[
"0x...",
"0x...",
"0x...",
...
]
(Note that currently Seth automatically creates reverted_transactions_<network>_<date>.json with all reverted transactions, so you can use this file as input for the trace command.)
- Decode named inputs
- Decode named outputs
- Decode anonymous outputs
- Decode logs
- Decode indexed logs
- Decode old string reverts
- Decode new typed reverts
- EIP-1559 support
- Multi-keys client support
- CLI to manipulate test keys
- Simple manual gas price estimation
- Fail over client logic
- Decode collided event hashes
- Tracing support (4byte)
- Tracing support (callTracer)
- Tracing support (prestate)
- Tracing decoding
- Tracing tests
- More tests for corner cases of decoding/tracing
- Saving of deployed contracts mapping (
address -> ABI_name) for live networks - Reading of deployed contracts mappings for live networks
- Automatic gas estimator (experimental)
- Block stats CLI
- Check if address has a pending nonce (transaction) and panic if it does
- 1password integration for keyfiles
- DOT graph output for tracing
You can read more about how ABI finding and contract map works here and about contract store here here.
This section explains how to configure and understand the automatic gas estimator, which is crucial for executing transactions on Ethereum-based networks. Here’s what you need to know:
Before using the automatic gas estimator, it's essential to set the default gas-related parameters for your network:
- Non-EIP-1559 Networks: Set the
gas_priceto define the cost per unit of gas if your network doesn't support EIP-1559. - EIP-1559 Networks: If your network supports EIP-1559, set the following:
eip_1559_dynamic_fees: Enables dynamic fee structure.gas_fee_cap: The maximum fee you're willing to pay per gas.gas_tip_cap: An optional tip to prioritize your transaction within a block (although if it's set to0there's a high chance your transaction will take longer to execute as it will be less attractive to miners, so do set it).
These settings act as a fallback if the gas estimation fails. Additionally, always specify transfer_gas_fee for the fee associated with token transfers.
If you do not know if your network supports EIP-1559, but you want to give it a try it's recommended that you also set gas_price as a fallback. When we try to use EIP-1559 during gas price estimation, but it fails, we will fallback to using non-EIP-1559 logic. If that one fails as well, we will use hardcoded gas_price value.
Gas estimation varies based on whether the network is a private Ethereum Network or a live network.
- Private Ethereum Networks: no estimation is needed. We always use hardcoded values.
For real networks, the estimation process differs for legacy transactions and those compliant with EIP-1559:
- Initial Price: Query the network node for the current suggested gas price.
- Priority Adjustment: Modify the initial price based on
gas_price_estimation_tx_priority. Higher priority increases the price to ensure faster inclusion in a block. - Congestion Analysis: Examine the last X blocks (as specified by
gas_price_estimation_blocks) to determine network congestion, calculating the usage rate of gas in each block and giving recent blocks more weight. - Buffering: Add a buffer to the adjusted gas price to increase transaction reliability during high congestion.
- Tip Fee Query: Ask the node for the current recommended tip fee.
- Fee History Analysis: Gather the base fee and tip history from recent blocks to establish a fee baseline.
- Fee Selection: Use the greater of the node's suggested tip or the historical average tip for upcoming calculations.
- Priority and Adjustment: Increase the base and tip fees based on transaction priority (
gas_price_estimation_tx_priority), which influences how much you are willing to spend to expedite your transaction. - Final Fee Calculation: Sum the base fee and adjusted tip to set the
gas_fee_cap. - Congestion Buffer: Similar to legacy transactions, analyze congestion and apply a buffer to both the fee cap and the tip to secure transaction inclusion.
Understanding and setting these parameters correctly ensures that your transactions are processed efficiently and cost-effectively on the network.
Finally, gas_price_estimation_tx_priority is also used, when deciding, which percentile to use for base fee and tip for historical fee data. Here's how that looks:
case Priority_Fast:
baseFee = stats.GasPrice.Perc99
historicalGasTipCap = stats.TipCap.Perc99
case Priority_Standard:
baseFee = stats.GasPrice.Perc50
historicalGasTipCap = stats.TipCap.Perc50
case Priority_Slow:
baseFee = stats.GasPrice.Perc25
historicalGasTipCap = stats.TipCap.Perc25All values are multiplied by the adjustment factor, which is calculated based on gas_price_estimation_tx_priority:
case Priority_Fast:
return 1.2
case Priority_Standard:
return 1.0
case Priority_Slow:
return 0.8For fast transactions we will increase gas price by 20%, for standard we will use the value as is and for slow we will decrease it by 20%.
We further adjust the gas price by adding a buffer to it, based on congestion rate:
case Congestion_Low:
return 1.10, nil
case Congestion_Medium:
return 1.20, nil
case Congestion_High:
return 1.30, nil
case Congestion_VeryHigh:
return 1.40, nilFor low congestion rate we will increase gas price by 10%, for medium by 20%, for high by 30% and for very high by 40%.
We cache block header data in an in-memory cache, so we don't have to fetch it every time we estimate gas. The cache has capacity equal to gas_price_estimation_blocks and every time we add a new element, we remove one that is least frequently used and oldest (with block number being a constant and chain always moving forward it makes no sense to keep old blocks).
It's important to know that in order to use congestion metrics we need to fetch at least 80% of the requested blocks. If that fails, we will skip this part of the estimation and only adjust the gas price based on priority.
For both transaction types if any of the steps fails, we fallback to hardcoded values.
There are multiple ways of visualising DOT graphs:
xdotapplication [recommended]- VSCode Extensions
- online viewers
To install simply run homebrew install xdot and then run xdot <path_to_dot_file>. This tool seems to be the best for the job, since the viewer is interactive and supports tooltips, which in our case contain extra tracing information.
There are multiple extensions that can be used to view DOT files in VSCode. We recommend using Graphviz Preview. The downside is that it doesn't support tooltips.
We were unable to find any (working) plugins for DOT graph visualization. If you do know any, please let us know.
There's at least a dozen of them available, but none of them support tooltips and most can't handle our multi-line labels. These two are known to work, though:
In order to enable an experimental feature you need to pass it's name in config. It's a global config, you cannot enable it per-network. Example:
# other settings before...
tracing_level = "reverted"
trace_outputs = ["console"]
experiments_enabled = ["slow_funds_return", "eip_1559_fee_equalizer"]Here's what they do:
slow_funds_returnwill work only incoreand when enabled it changes tx priority toslowand increases transaction timeout to 30 minutes.eip_1559_fee_equalizerin case of EIP-1559 transactions if it detects that historical base fee and suggested/historical tip are more than 3 orders of magnitude apart, it will use the higher value for both (this helps in cases where base fee is almost 0 and transaction is never processed).