Most Ethereum wallets show you a hex string and a gas estimate. You click confirm.
That's not enough if you care about what you're actually signing.
veil-cli is an open-source, terminal-first security tool for EVM transactions. The goal: before your private key touches anything, you should see a decoded function name, a risk score, and a simulated balance diff. This post covers how it was built — from the first decode command to a full send pipeline.
Part 1: Decode, simulate, and score before you sign
The core problem is that raw calldata is opaque. 0xa9059cbb000000000000000000000000... means nothing to a human. Wallets render it as "contract interaction" and move on.
veil decode resolves the ABI from three sources in order:
- Etherscan — if the contract is verified and you have an API key
- Sourcify — decentralized, no key required
- 4byte.directory — fallback by selector hash, covers most common functions
veil decode 0xabc123... --chain mainnet
veil decode 0xa9059cbb... --address 0xdAC17F... --chain mainnet
veil risk runs a heuristics pass on the destination contract: proxy detection, bytecode size checks, EOA detection, plus GoPlus Security API data for honeypot flags, blacklists, and high sell tax.
veil simulate forks the chain locally with Anvil and shows you balance diffs before the transaction is broadcast. One thing that surprised me: transactions that would revert on-chain still pass RPC validation — the node accepts them. You only find out after paying gas. The local fork catches it for free.
Stack: TypeScript, viem, Commander.js, Ink, Foundry/Anvil, GoPlus Security API.
Part 2: Keystore v3 with zero new dependencies
A security tool that handles private keys needs a reasonable storage model. We implemented Ethereum keystore v3 — the same format used by geth, MetaMask, and MyCrypto.
The format is three steps:
- Derive a key from the password using
scrypt - Encrypt the private key with
AES-128-CTRusing the first 16 bytes - Compute a MAC over the last 16 bytes + ciphertext to detect wrong passwords
Everything is in node:crypto — except keccak256 for the MAC, which we pulled from viem since it was already a dependency. No new packages.
Two things worth noting from the implementation:
crypto.scryptSync() blocks the event loop for 1–2 seconds with N=131072. Acceptable for a CLI, but we switched to the async version anyway. The default maxmem of 32MB in Node isn't enough for these parameters — we had to raise it to 160MB. That one wasn't obvious from the docs.
The MAC comparison uses crypto.timingSafeEqual() instead of a plain string comparison. Is a timing attack against a local CLI keystore a realistic threat? Probably not. But if you're writing a security tool, it's hard to justify doing it the wrong way.
veil wallet create # generates a key, encrypts with password, writes to ~/.veil/wallets/
veil wallet import # same flow, bring your own private key
veil wallet list # shows all wallets and their addresses
Output is a standard keystore v3 file — importable into MetaMask or any compatible client.
Part 3: Sign only after you understand
This is where everything comes together. veil send runs a full pipeline before your key is ever unlocked:
tx.json → decode → risk check → simulate → summary → confirm → password → broadcast
Here's what the summary looks like before the prompt appears:
┌ Transaction summary ─────────────────────────────────
│ From 0xYourWallet
│ To 0xUniswapRouter
│ Value 0.5 ETH
│ Method swapExactETHForTokens
│ Risk ✔ LOW
│ Gas ~142,381
└──────────────────────────────────────────────────────
ETH -0.500000
PEPE +2,184,112.000000
? Sign and broadcast? › (y/N)
Your key is never unlocked until the last step. By the time you type your password, the transaction is already boring.
Why tx.json?
Instead of constructing a transaction inline, veil send takes a file:
veil send tx.json --chain mainnet
That means a transaction can be built, reviewed, and version-controlled before signing is even part of the process. veil tx build is an interactive wizard that generates the file. A Gnosis Safe export or cast calldata output works just as well.
The uncomfortable reality of unverified contracts
Many contracts are not verified. No source code on Etherscan, nothing on Sourcify, selector unknown to 4byte.directory.
Most tools handle this by showing nothing useful — or by proceeding as if the decode succeeded. Neither is honest.
veil send doesn't block the pipeline on an unreadable contract. Risk check and simulation still run. But instead of pretending the decode worked, it shows the raw calldata and makes the gap explicit. You see what the tool knows and what it doesn't. The decision stays with you.
What the risk check still misses
The current engine catches obvious things: honeypot flags, unverified source, high sell tax, blacklisted addresses. It misses subtler issues — an upgradeable proxy pointing at a new implementation, a multisig with recently changed signers.
That's an open problem. Detecting those cases on-chain requires either event log indexing or off-chain data sources. Haven't found a clean solution yet.
The goal
The goal isn't to make signing harder.
The goal is to move understanding before authorization.
By the time the password prompt appears, the transaction should already be boring.
veil tx build # interactive wizard → tx.json
veil send tx.json # full pipeline, sign only at the end
Current state
veil decode <tx-hash|calldata> # decode any transaction
veil approvals <address> # scan active ERC-20/721 approvals
veil simulate <tx.json> # local fork, balance diffs
veil risk <address> # contract risk score
veil wallet create/import/list # encrypted local keystore
veil tx build # interactive tx.json builder
veil send <tx.json> # full pipeline, sign last
Not published to npm yet — install from source for now.
github.com/summusforge-lab/veil-cli
Originally posted as a series on r/ethdev.

Top comments (0)