Experimental Ethereum CLI built on Foundry and Alloy.
Right now the main implemented command is:
ethx send— acast send-like transaction sender
It also supports wrapping calls through a smart-account encoder, currently:
safe
ethx send \
--rpc-url https://rpc.example \
--private-key <KEY> \
0xTarget \
"transfer(address,uint256)" \
0xRecipient \
1000000000000000000ethx send \
--rpc-url https://rpc.example \
--private-key <KEY> \
--create \
0x6080...This uses the encoder abstraction to wrap the inner call into Safe execTransaction(...) calldata
and send the outer transaction to the Safe itself.
ethx send \
--rpc-url https://rpc.example \
--private-key <OWNER_KEY> \
--encoder safe \
--target 0xSafe \
--safe-eoa-signature 0x<owner_sig> \
0xInnerTarget \
"transfer(address,uint256)" \
0xRecipient \
1etherYou can also supply other Safe signature forms:
--safe-contract-signature 0xOwnerContract:0x<1271_sig>
--safe-approved-hash 0xOwnerMeaning of addresses:
TOpositional argument: inner call destination--target: Safe address
ethx send can either send a transaction directly, or first pass the requested call through an
encoder.
Without an encoder:
- the positional
TOaddress is the transaction recipient - the parsed
sig + argsbecome the final calldata
With an encoder:
- the positional
TOaddress is the inner call destination - the parsed
sig + argsbecome the inner calldata --targetis the outer account/contract that actually receives the transaction- the selected encoder transforms that inner call into the outer calldata expected by the target
This is meant to support smart-account style flows where the final onchain transaction is sent to an account contract, but still describes some inner call to be executed by that account.
Today the only implemented encoder is safe, which wraps the inner call into Safe
execTransaction(...) calldata.
From an implementation perspective, the abstraction is centered on:
RawCall: a normalized inner call shape (to,value,data)CallEncoder: a trait that transforms aRawCallinto anotherRawCallCallEncoder::EncodeContext: encoder-specific extra state needed during encoding
That lets each encoder decide:
- how the outer calldata should be built
- whether the outer recipient should differ from the inner recipient
- what extra preparation/validation is needed before encoding
The Safe implementation uses this to attach Safe-specific context such as:
- operation mode
- Safe gas/refund fields
- structured signature inputs
- prepared packed signature bytes
The intent is that other smart-account/account-abstraction styles can be added later by:
- defining their own context type
- implementing
CallEncoderfor their wrapper format - adding CLI parsing/mapping for their encoder-specific options
This is an experiment, not a polished replacement for cast.
Implemented today:
- Foundry-style function signature + args parsing via
foundry_cli::utils::parse_function_args - Foundry wallet resolution via
foundry-wallets - regular call sending
- contract creation via
--create - unlocked sending via
eth_sendTransaction - signer-based sending
- Safe
execTransaction(...)wrapping - Safe tx hash derivation for:
1.0.01.1.11.2.01.3.01.4.11.5.0
- structured Safe signature inputs:
- EOA signatures
- EIP-1271 contract signatures
- approved-hash signatures
- Safe owner/threshold validation and optional auto-add of the current signer signature
- Safe packed signature encoding, including dynamic offsets for contract signatures
nix-shell -p cargo rustfmt clippy --run 'cargo build'cargo buildcargo run -- --help
cargo run -- send --helpFor the Safe encoder, ethx currently:
- queries Safe metadata:
VERSION()nonce()getThreshold()getOwners()- batching via multicall when available, with fallback to direct calls
- derives the Safe tx signing hash locally
- supports these input forms:
--safe-eoa-signature <SIG>--safe-contract-signature <OWNER>:<SIG>--safe-approved-hash <OWNER>
- validates EOA signatures by recovering the owner from the Safe tx hash
- validates EIP-1271 signatures by calling:
owner.isValidSignature(safeTxHash, signature)
- validates approved-hash signatures by checking:
- executor is the owner, or
approvedHashes(owner, safeTxHash) != 0
- verifies referenced/recovered owners are actual Safe owners
- sorts signatures by owner address ascending
- checks threshold early
- if the provided owner signatures are below threshold and a local signer is available, signs the Safe tx hash with the current signer and appends that signature when appropriate
- encodes the final Safe packed signatures payload, including contract-signature offsets and dynamic tails
Supported today:
- standard recoverable 65-byte EOA signatures
- EIP-1271 contract signatures
- approved-hash signatures
Not supported yet:
- Safe
v == 2P-256 / RIP-7212-style signatures
- The local variable named
ens_chainin the code is currently only used for Foundry parsing / resolution behavior, not as the tx chain id. - Transaction filling is still mostly explicit/manual at the moment.
Useful commands:
cargo fmt --all
cargo test
cargo check
cargo clippy --all-targets --all-features -- -D warnings