clear-signing-helper
v0.6.0-preview.2
Published
Author and test ERC-7730 clear-signing descriptors in Foundry repositories
Readme
Clear Signing Helper
Create, preview, and regression-test ERC-7730 clear-signing descriptors alongside a Foundry project.
Use it to turn compiled contract functions into editable ERC-7730 JSON drafts, inspect the fields a transaction would display, and catch changes to signing output in CI. Descriptors and fixtures stay in your contract repository.
Developer preview: 0.6.0-preview.2. Drafts are built from what the repository proves (ABI, NatSpec, compiler AST, broadcast records, verified source), accept every format the registry does, and export registry-shaped bundles checked by the registry's own linter. Works with Foundry projects or from a verified address. Generated labels, units and intent require developer review. Independent human review, protocol-maintainer assessment, physical-device testing and production wallet delivery remain pending. The CLI does not sign or send transactions.
Overview site · Install · Try an example · Use your own project · Commands
Install the developer preview
Requires Node.js 22+ and Foundry (or a verified contract address for ABI mode). Install the npm preview:
npm install --global --ignore-scripts clear-signing-helper@preview
clear-signing --versionOr try the CLI without a global installation:
npx --yes --ignore-scripts --package=clear-signing-helper@preview clear-signing --helpPin [email protected] for reproducible installs. This is an experimental developer preview; specify the preview tag or exact version explicitly.
Expected version: 0.6.0-preview.2. Installation is independent of your contract repository; it does not need a package.json or Node dependencies. Have forge available on your PATH.
Continue with the example below or your own Foundry project. Existing users should follow the upgrade procedure. For other installation methods, see building from source and verified tarballs.
Try the included example
After installing the CLI, clone the example from the matching release and build its contracts:
git clone --depth 1 --branch v0.6.0-preview.2 \
https://github.com/portdeveloper/clear-signing-helper.git
cd clear-signing-helper
forge build --root examples/standard
clear-signing --root examples/standard preview \
--fixture clear-signing/fixtures/deposit.jsonThis example includes an editable vault descriptor, a deposit fixture, and local token/address metadata. It needs no wallet, RPC endpoint or funded account. The example files live in the source repository; installing the npm package alone does not create them.
To view the same transaction in your browser:
clear-signing --root examples/standard preview \
--fixture clear-signing/fixtures/deposit.json --openThe browser preview prints a private loopback URL and opens it in your browser. On a remote machine or without a desktop browser, use the terminal preview above. Use --serve instead of --open to print the URL without launching a browser. Stop the server with Ctrl+C.
Expected fields:
Deposit (local draft)
Amount: 1 USDC
Receiver: Alice
Address: 0x0000000000000000000000000000000000000002These example addresses and token metadata are local fixtures. The automated Anvil test separately deploys contracts, mines a real local deposit, renders its actual calldata, and checks resulting balances.
Add clear signing to your Foundry repository
Run these commands from your contract repository. Build it once with your existing Foundry setup so the Solidity compiler and project dependencies are available; the helper subsequently uses forge build --offline --ast with NatSpec outputs, which may recompile once. If a compiler is missing, the helper says which one and asks for that single forge build.
The example below assumes your project has src/Vault.sol:Vault with deposit(uint256,address). Replace those with your actual compiled contract identity, function signature and sample arguments. The addresses below are local example values.
forge build
clear-signing init --contract src/Vault.sol:Vault --owner "My Protocol"
clear-signing fixture \
--name deposit \
--contract src/Vault.sol:Vault \
--function 'deposit(uint256,address)' \
--args '["1000000","0x0000000000000000000000000000000000000002"]' \
--chain-id 31337 \
--to 0x0000000000000000000000000000000000000001 \
--local
clear-signing preview --fixture clear-signing/fixtures/deposit.jsonOmit --contract during init to select production contracts under the resolved source directory. Repeat it to select several contracts. Tests, scripts, interfaces, abstract contracts, compile-only stubs with no entry points, and dependencies are excluded from default selection. Dependencies can be selected explicitly by their compiled identity.
Drafts are built from what the repository itself states or proves, and init prints where each value came from:
- NatSpec. A function's
@noticebecomes the intent when its first sentence fits the registry linter's 30-character limit;@paramtext becomes a field label under a 32-character rule. Longer text is reported but not used. - Solidity enums. A parameter declared as an enum gets the
enumformat with its members undermetadata.enums, read from the compiler AST. - Constructor constants. An
immutableaddress assigned from a constructor argument, with that argument recorded in a broadcast, becomesmetadata.constants.<name>. Reference it as$.metadata.constants.<name>in atokenparameter. Values that differ between deployments are left out. - ERC-20 and WETH conventions. A contract with the full ERC-20 surface gets the registry's usual
approve,transferandtransferFromformats denominated in the contract itself; a WETH-shaped contract also getsWrapandUnwrap. These are conventions, marked as such, and editable like any draft. TheinterpolatedIntentuses the registry's phrasing when it fits 30 characters with the contract's parameter names, a shorter one when it does not (Let {spender} spend {value}), and none when nothing fits; provenance says which.
init reads the repository's broadcast/ records. A contract created by forge script --broadcast gets its deployments binding filled from that record, and deployed contracts outside the default selection, such as a router under lib/, are listed as suggestions with their addresses. Dry runs are ignored, and a contract name shared by several compiled contracts is never bound automatically.
init prints the generated descriptor path under clear-signing/descriptors/; fixture writes clear-signing/fixtures/deposit.json. If a forge script --broadcast run already sent the transaction you want to describe, fixture --name <n> --contract <id> --broadcast-tx <hash> copies its chain, target, calldata and value verbatim. With only an address, fixture --name <n> --contract <id> --tx <hash> --rpc-url <url> copies a mined transaction from the chain the endpoint serves. Take the hash from an explorer, or send one yourself. The transaction must have succeeded, must have been sent to a bound deployment of the contract (a call through a router, multicall or smart account has the outer contract as its target), and must carry canonical calldata; anything else is refused and no fixture is written. It reads eth_chainId, eth_getTransactionByHash and eth_getTransactionReceipt, plus eth_call for token metadata (below), nothing else. Either way the fixture keeps the transaction's hash, and export writes it as the test case's txHash, so a reviewer can look the transaction up. --local marks an undeployed example binding, so you can preview before adding real deployment addresses. With --rpc-url (with or without --tx), the fixture's missing token metadata is read from the chain: every token its rendering asks for and the fixture lacks gets symbol(), decimals() and name() through read-only eth_call, written into tokens (an endpoint on another chain is refused; a contract that does not answer as an ERC-20 is listed under unresolvedTokens, not guessed).
Drafts use raw values for every argument. The first preview shows an amount such as 1000000 until you add its token formatting and metadata. Inspect the generated JSON before choosing units or action descriptions. For a vault deposit, edit the generated display.formats entry like this:
"deposit(uint256 assets,address receiver)": {
"intent": "Deposit",
"fields": [
{
"path": "assets",
"label": "Amount",
"format": "tokenAmount",
"params": { "token": "0x0000000000000000000000000000000000000003" }
},
{
"path": "receiver",
"label": "Receiver",
"format": "addressName",
"params": { "sources": ["local"] }
}
]
}Use the actual parameter names from your artifact; the tool checks them. Add metadata to the fixture:
"tokens": {
"0x0000000000000000000000000000000000000003": {
"name": "USD Coin", "symbol": "USDC", "decimals": 6
}
},
"addressNames": {
"0x0000000000000000000000000000000000000002": "Alice"
}The token address must correspond to the asset the contract actually uses. An ABI cannot establish that relationship. Fixture metadata applies to the fixture's chain and is never fetched from RPC.
Run the preview again after editing the descriptor and fixture:
clear-signing preview --fixture clear-signing/fixtures/deposit.jsonAfter inspecting the source, descriptor, and rendered fields:
clear-signing review --accept
clear-signing test --update
clear-signing check --strict-portability
clear-signing testreview --accept records your acknowledgement of the current source, descriptions, and exclusions. test --update accepts current rendering expectations. They are separate explicit actions; ordinary check/test runs do not modify tracked authoring.
Commit clear-signing.toml and clear-signing/. On subsequent changes, run clear-signing sync to scaffold newly added functions, inspect the changes, and renew review/expectations deliberately. Run strict check and test in CI to detect stale review and changed output. A local fixture supports preview and testing; export additionally requires real deployment bindings and fixture coverage for every covered function.
Use without Foundry: ABI mode
A Hardhat project, or a deployment you only know by address, gets the same fixture, test and export workflow. Run in an empty directory (or pass --root):
clear-signing init --address 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --chain-id 1 --owner "WETH"
clear-signing init --abi ./artifacts/Vault.json --name Vault --owner "My Protocol"--address is the one network call init makes, and only when asked. It fetches the verified source match from Sourcify, falling back to Etherscan V2 when ETHERSCAN_API_KEY is set. A verified proxy is resolved to its implementation: the descriptor describes the implementation's functions and binds the proxy address users actually call. NatSpec from the verified source feeds intents and labels the same way Foundry artifacts do. Address immutables with a public getter, such as a staking contract's stakingToken, become metadata.constants (tagged verified source in provenance), so an amount can be denominated as $.metadata.constants.stakingToken. Sourcify's record carries each immutable's value in the verified bytecode but no AST to name it. The tool names it from the verified source map: every place the bytecode reads the immutable maps to its declaration or its identifier. An immutable whose references disagree, whose value is not an address, or that has no getter is left out rather than guessed. Nothing extra is fetched: the same Sourcify request carries the bytecode and sources. If that record is too large, the ABI is imported without constants. The verified source files from the same record are written to clear-signing/source/<Name>/ for reading while you decide intents and denominations; nothing reads them and they are not fingerprinted. --registry <registry-clone> makes init compare selectors against that clone instead of the bundled snapshot, so a descriptor merged after this release is found too. An unverified address is refused with the reason; verify it on Sourcify or pass the ABI you trust with --abi.
Imported ABIs are stored under clear-signing/abi/<Name>.json with a <Name>.source.json sidecar recording where they came from, when, the match status, and any proxy resolution. Commit both. The engine fingerprints those files instead of Solidity sources, so editing an ABI invalidates review exactly as a source change would. clear-signing.toml records mode = "abi"; a directory cannot mix modes.
Not available in ABI mode: Solidity enums, constructor constants, broadcast bindings and broadcast fixtures, since those come from the compiler AST and forge script records.
Add a chain to a descriptor already in the registry
Most protocols a new chain cares about already have a registry descriptor. The pull request that adds the chain is one deployment entry plus one test case, and the tool produces both from a registry clone:
git clone https://github.com/ethereum/clear-signing-erc7730-registry.git
clear-signing registry add-deployment \
--registry clear-signing-erc7730-registry \
--descriptor registry/weth/calldata-weth.json \
--chain-id 8453 --address 0x4200000000000000000000000000000000000006 \
--token 0x4200000000000000000000000000000000000006=WETH:18The address is proven to be the same contract before anything changes: its verified ABI is fetched from Sourcify (Etherscan V2 with ETHERSCAN_API_KEY as fallback, a verified proxy followed to its implementation) and every function the descriptor formats must exist in it by selector. The deployment is appended, and a test case for the new chain is rendered from an existing case's calldata through the pinned renderer, so expected values are computed rather than copied. Token symbols and address names differ between chains and are never guessed; supply them with --token <address>=<SYMBOL>:<decimals> and --address-name <address>=<Name>, or pass --no-test to add the deployment alone. Edits are inserted into the existing file text, copying the formatting of the neighbouring entry, so the pull request diff is the added lines and nothing else. If no existing test case can be rendered on the new chain, the command says why for each one and leaves both files untouched. Upstream lint runs on the result, and the exact git and gh commands are printed; the tool never commits or opens the pull request.
EIP-712 descriptors have no ABI to check, so the proof is the contract's own domain separator, read through an RPC endpoint you name:
clear-signing registry add-deployment \
--registry clear-signing-erc7730-registry \
--descriptor registry/uniswap/eip712-uniswap-permit2.json \
--chain-id 143 --address 0x000000000022D473030F116dDEE9F6B43aC78BA3 --rpc-url https://rpc.monad.xyz \
--set details.token=0x754704Bc059F8C67012fEd69BC8A327a5aafb603 --set spender=0x1b81D678ffb9C0263b24A97847620C99d213eB14 \
--token 0x754704Bc059F8C67012fEd69BC8A327a5aafb603=USDC:6The tool follows includes to the file that lists the deployments, checks that the endpoint serves --chain-id and that there is code at the address, and requires DOMAIN_SEPARATOR() to equal the descriptor's domain (or one of its existing tests' domains) for that chain and address. It then inserts the deployment in chain-id order and retargets an existing test case at the new chain. --set <path>=<value> overrides an existing message field, and --template picks the case. The case is rendered through the pinned renderer the way the registry's Sourcify runner does, and upstream lint, with the package and flags from the clone's own CI definition, runs on every descriptor that includes the edited file. A lint error or a failed registry runner puts every edited file back as it was. Message addresses still copied from the template are listed as templateAddresses, because test token metadata is not keyed by chain. The only RPC calls are eth_chainId, eth_getCode and eth_call. A matching domain shows the contract signs under that domain; it is not a bytecode comparison. The command above reproduces registry PR #2611 byte for byte.
Decisions: the slots only judgment can fill
Everything the scaffold cannot prove is exposed as one editable file per contract, so a person or an agent fills it without touching descriptor JSON:
clear-signing decisions --contract src/Vault.sol:Vault # writes clear-signing/decisions/Vault.json
# fill intents, formats, denominations, show/hide with reasons, exclude with reasons; set "author"
clear-signing apply --decisions clear-signing/decisions/Vault.jsonThe template lists every function and every argument leaf with its current value and read-only hints: the NatSpec text, the registry's formats for the same selector, candidate denominations for amounts (@.to, $.metadata.constants.*, address arguments, a literal token), and the formats valid for the type. At the top, owner, contractName, url and enums (enum name to {"value": "Label"}, referenced from a field as {"$ref": "$.metadata.enums.<name>"}) go to metadata. In a descriptor without groups, fields are written in the order of their keys, so moving a key moves the field. receive() has no entry (it needs none); fallback() has an exclude-only one. apply validates the result before writing anything, then writes the descriptor, exclusions and hidden in clear-signing.toml (a hidden argument stays in the descriptor as a visible: "never" field, as registry descriptors write it, so the registry linter counts it and reviewers see it; the reason stays in clear-signing.toml), and records each decision in clear-signing/provenance.json as human or llm according to the file's author. Hidden native value and unsupported formats are refused. Running decisions again reproduces the current state and keeps the file's author, so the file round-trips. A later decision replaces the earlier one for the same slot in provenance, and a removed interpolated intent is recorded as removed.
Files and commands
clear-signing.toml
clear-signing/
descriptors/calldata-Vault-<identity-hash>.json
fixtures/deposit.json
expectations/deposit.json
review.json
provenance.json where each intent, label and format came from (natspec, ast, broadcast, convention, registry, human, llm)
decisions/<Name>.json the judgment slots for one contract, filled by a person or an agent
abi/<Name>.json ABI mode only, with <Name>.source.json beside it
.clear-signing-cache/build.jsonCommit the TOML file and clear-signing/. Ignore .clear-signing-cache/ and your existing Foundry output/cache directories. Review records are tied to the effective build and exact tool/renderer/schema versions; update them deliberately when those inputs change.
| Command | Behavior |
| --- | --- |
| init | Scaffold selected contracts; existing descriptors are preserved. --abi <file> or --address <addr> --chain-id <id> starts ABI mode; --registry <clone> matches against a registry clone |
| upgrade | Adopt a new engine version, preserve descriptors/expectations, and invalidate old review |
| decisions --contract <id> | Write the judgment slots (intent, formats, denominations, show/hide, exclude) as a file with hints |
| apply --decisions <file> | Validate a filled decisions file, then write descriptor, exclusions, hidden reasons and provenance |
| sync | Add missing function formats; report obsolete references without deleting them |
| fixture | Encode a sample call using the compiled ABI, or copy a real transaction: --broadcast-tx <hash> from a broadcast record, --tx <hash> --rpc-url <url> from the chain; --rpc-url also fills missing token metadata |
| preview --fixture <path> | Render in the terminal; --open / --serve enables a browser preview |
| review --accept | Record review for all selections; --contract narrows it |
| check | Validate schema, supported features, ABI coverage, and review freshness; --contract narrows it |
| test | Render all fixtures and compare saved expectations |
| test --update | Accept expectations only if every fixture renders without blocking warnings; --contract narrows both |
| export --out <directory> | Validate, write a registry-shaped bundle, and run upstream lint; --contract exports one contract; never publishes |
| registry add-deployment | Add a verified deployment and a rendered test case to a descriptor in a registry clone; --runners checks it with the registry's implementations; never commits |
| registry setup-runners | Clone and build the registry CI's Sourcify and Rust implementations once |
Global options: --root <directory>, --profile <name>, --no-build, and --json. Relative fixture/config paths resolve from the project root (the nearest clear-signing.toml or foundry.toml), including when invoked in a subdirectory. The saved profile is the default; an explicit flag or FOUNDRY_PROFILE overrides it.
check prints warnings grouped by descriptor and code, the ones a signer would notice first (undisplayed arguments, token mapping, type mismatches), then what registry lint reports (intent length), then advice (registry priors), with each code's remedy once. An argument a displayed field reads through tokenPath, token, collectionPath or chainIdPath counts as displayed, as it does in the registry linter.
init also checks whether the registry already describes the contract. It compares the contract's function selectors with every registry calldata descriptor in the same snapshot and lists up to three matches ("Already in the registry?", registryMatches in --json). A match needs at least two shared functions, and either every function the file describes exists in the contract, which is what registry add-deployment's selector proof requires, or the file covers at least half of the contract's functions. Functions that three or more registry entities describe (ERC-20, ERC-4626, ERC-721, Ownable, multicall) name an interface, not a protocol, and do not count toward the overlap. A match is a suggestion. It cannot tell the same protocol on a new chain (use add-deployment) from a fork, whose file belongs to another owner, and nothing is bound or skipped on it. Measured at registry 8f56072 and again at 53d86dc (2026-09-25), with the same results: a Uniswap V2 router finds quickswap/calldata-QuickSwap.json, SwapRouter02 finds uniswap/calldata-UniswapV3Router02.json, and none of the 64 puddleswap artifacts other than router interfaces match anything.
check also compares each format against what other registry descriptors do with the same function selector, using a pinned snapshot of the registry's display formats. When every prior, from at least two registry entities, types or hides an argument and the draft leaves it raw or shown, check warns (CORPUS_DISAGREEMENT); a single entity's choice, often for an unrelated function that shares the selector, stays a hint; init lists the priors it found under the draft's evidence. This is advisory: the registry's choices for one contract are a strong hint for another with the same selector, not proof. Refresh the snapshot with npm run priors:snapshot -- <registry-clone>.
check and export report known portability issues for nested arrays, tuple-array field ordering and signed integers. Use --strict-portability on either command to reject those ABI shapes in CI. Ordinary export includes portability.json for review. No findings does not mean a wallet has been verified; see the tested device matrix.
--no-build works only after a successful helper build and an exact match of current source/config/artifact fingerprints. A missing cache, changed source, or modified artifact requires rebuilding. Source changes without ABI changes also invalidate review. Fingerprinting is conservative across the project's discovered artifacts and Solidity inputs, so unrelated project source changes may require review too.
For a deliberate function exclusion, remove its descriptor format and record a reason in the selection's exclusions table in clear-signing.toml. For a deliberately hidden argument, record a reason in the hidden table (see Supported boundary):
[contracts.exclusions]
"transferAdmin(address)" = "Handled in the separate administrative signing workflow"TOML's table applies to the preceding [[contracts]] entry. Admin functions are inventoried by default. receive() needs no entry: it runs only on empty calldata, so there is nothing to describe. fallback() takes calldata that no calldata descriptor can format, so it needs an exclusion reason (the decisions file has a slot for it).
Production bindings and registry export
For a deployed contract, put real bindings in its descriptor and remove localBinding from the associated fixtures:
"context": {
"contract": {
"deployments": [{ "chainId": 1, "address": "0xYourActualContractAddress" }]
}
}The placeholder above is illustrative and fails validation until replaced with a valid address. A proxy descriptor must bind to the user-facing proxy address while the selected artifact describes its implementation. This mapping is explicit; the tool does not verify deployed bytecode or discover proxies.
Preview must match the fixture's chain and target against the descriptor. localBinding: true works only with an empty deployment list and cannot be exported. Adding production bindings requires new review and expectation acceptance.
clear-signing check --strict-portability
clear-signing test
clear-signing export --strict-portability --out dist/clear-signingExport requires a production binding, current review, and at least one passing fixture for every covered function of each exported contract. Pass --contract <id> (repeatable) to export one contract while other selections are still drafts. The bundle is laid out the way the registry expects:
<out>/
registry/<entity>/calldata-<ContractName>.json relative $schema, as in the registry
registry/<entity>/testsv2/calldata-<ContractName>.tests.json
review/fixtures/ review/renderings/ review/validation.json review/portability.json
README.md copy instructions and lint status<entity> defaults to a kebab-case slug of metadata.owner; pass --entity <slug> to match an existing registry folder. Test descriptions follow the registry's <fixture> - chain <id> form. --inline-abi embeds the compiled ABI, which the schema marks deprecated, so it is off by default.
Export then formats the descriptors with the registry's own erc7730 format (the registry's format bot would otherwise rewrite them) and runs its linter through uvx, the way the registry's CI does: the erc7730 package from the registry's .github/requirements.txt (a Sourcify fork pinned by commit) with the flags its pull-request workflow passes. The built-in pin follows the registry's CI after #3038, so --require-verified is on and every deployment must be verified on Sourcify; --registry <registry-clone> reads the pin from a clone instead. A lint error removes the bundle and fails the export; warnings are recorded in review/validation.json. Without uvx the exact command is printed instead, and --no-lint skips it. check already warns locally when an intent exceeds the linter's 30-character limit.
Add --registry-runners to also run the two implementations the registry's CI tests every submission against: the Sourcify TypeScript runner and the Rust cs-test runner, at the revisions the registry pins. The first use clones and builds them under ~/.cache/clear-signing-helper/runners/, or $CLEAR_SIGNING_RUNNERS_DIR when set (needs git, npm and cargo; clear-signing registry setup-runners does this ahead of time). Every case must pass in both, or the export fails and the bundle is removed. The two implementations sometimes disagree with each other (at the pinned revisions the Rust runner names Polygon's native currency MATIC and the Sourcify runner POL, registry #3027); then --accept-runner-failure <sourcify|rust>=<reason> exports as long as the other runner passes, keeps the failing runner's output, and records the reason in review/validation.json and the bundle README. registry add-deployment --runners takes the same option. Results and logs land in review/runners/. --registry <registry-clone> reads the runner revisions (and the lint pin) from a clone's CI definition instead of the built-in pins. registry add-deployment --runners does the same for the test file it updates, and restores the clone if any case fails.
The runners are required, not optional, when a displayed field is a signed integer or a nested array. On those shapes the expected values come from this tool's renderer, which carries a signed-integer fix and flattens nested arrays before rendering, and the registry's runners can render them differently. export and registry add-deployment then stop with REGISTRY_RUNNERS_REQUIRED unless the runners run, or --skip-registry-runners "<reason>" records why they did not; the reason goes into review/validation.json and the bundle README. Hiding the field (with a reason) also clears it.
--out names a new directory inside the project; an absolute path inside it is accepted, anything outside is refused with that reason (copy the bundle afterwards). Export refuses existing directories, escaping paths, two selected contracts with the same name, and conflicting metadata across fixtures for the same descriptor. Copy registry/<entity>/ into a registry clone and open the pull request from an account tied to the contract owner. Deployed-code verification, registry review, attestations, and wallet distribution remain separate steps. Submission guide
CI
After installing the pinned tool, Foundry/compiler version, and project dependencies:
clear-signing check --strict-portability --json
clear-signing test --jsonDo not put review --accept or test --update in CI. Exit codes are 0 for success, 1 for validation/rendering/test failures, and 2 for invocation or environment failures. JSON diagnostics include stable codes and file/signature/remediation details when available.
See the consumer CI example and this project's validation workflow.
Supported boundary
The CLI pins ERC-7730 v2 schema 2.0.0 and Sourcify renderer 0.2.2 with a reproducible signed-integer decoding fix. Patch details A descriptor passes check when it satisfies the pinned schema and uses only formats the renderer implements:
- Formats:
raw,amount,tokenAmount,addressName,nftName,date,duration,unit,enum,tokenTicker,chainId, with the parameters the schema defines for each.calldata(nested calls) andinteroperableAddressNameare rejected explicitly.tokenPath,collectionPathandnativeCurrencyAddressmay point at@.to, a literal address,$.metadata.token,$.metadata.constants.*, or any argument the renderer can read an address from, including array elements such aspath.[0]andpath.[-1]and byte slices such asto.[-20:]. - Registry idioms are accepted as the renderer accepts them:
#.root prefixes,$id,display.definitionswith$ref, constant-value fields,visibleset toalways,never,optional,defaultor anifNotIn/mustMatchrule, relative paths scoped inside groups, and byte slices on leaves. A format's type mismatch (anaddressNameon astring, say) is a warning because the renderer renders it raw with a warning; snapshot acceptance still refuses the resulting rendering. Measured against the registry:npm run registry:acceptance -- <clone>reports how many merged descriptors the validator accepts; the only class it rejects by design is thecalldataformat. - Calldata write functions, overloads, inherited functions, tuples, fixed/dynamic nested arrays, and sequential groups that keep tuple members paired. Nested groups are expanded to concrete indexed paths for each local preview; exported descriptors retain standard nested groups, whose support varies across wallets.
- Argument leaf paths and
@.to,@.from,@.value. Payable functions must display@.value; drafts use theamountformat for it. - The registry linter's Ledger length limits, as warnings: intents 30 characters, field and group labels 20, enum entries 20, owner 22, contract name 30, URL 26, legal name 30.
interpolatedIntent: an optional sentence with{path}placeholders naming shown fields, e.g."Stake {amount}". Wallets prefer it overintentand the registry recommends one on every format. Placeholders must name displayed fields (or@.value); the rendered sentence is checked into expectations and intotestsv2, where the registry's runners compare it. An argument left out of a format is reported as a warning. Hide it throughdecisions/apply, which writes avisible: "never"field and records the reason; or record the reason by hand under the selection'shiddentable inclear-signing.toml(the registry linter still warns about an argument with no field, so preferapply):
[contracts.hidden."swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"]
"path.[]" = "Route is implied by the input and output token amounts"- Local metadata only. Fixtures may carry
tokens,addressNames,ensNames,nftCollectionNames,blockTimestamps, and achainentry with the native currency. Well-known chains, including Monad, fall back to a built-in native-currency table; anything else needschainin the fixture, or--chain-nameand--native-currencywhen creating it. Rendering fetches nothing;fixture --rpc-urlis the one step that reads token metadata from a chain, into the fixture. - Exact, canonical ABI calldata up to 64 KiB. Input files are bounded to 8 MiB; descriptor/fixture trees to 32,768 nodes and 96 levels; decoded arguments to 8,192 nodes and 32 levels; signing output to 4,096 fields/groups.
Human previews escape terminal controls and bidirectional overrides as visible text. JSON and saved expectations retain the original values. The renderer is vendored with provenance and dependency notices; installation does not patch node_modules.
Missing token metadata is visible as warnings and raw fallback in preview, and blocks test acceptance/export. Empty-array notices and unnamed addressName values are informational: the address renders in full, as wallets show it, and the warning remains in expectations. Dates beyond the renderer's supported range fail instead of being silently accepted. Registry test runners use their own chain tables; an amount field on a chain they do not know will render raw there.
Unsupported features fail explicitly: EIP-712 authoring (adding a chain to an existing EIP-712 descriptor is supported), nested transaction decoding (the calldata format), external includes/references, encryption, factory bindings, and automatic registry access. Apart from init --address, registry add-deployment (including the read-only RPC calls behind --rpc-url), fixture --rpc-url (read-only RPC: the transaction for --tx, token metadata), the upstream lint and format run on export and add-deployment, and the one-time runner build behind --registry-runners, nothing touches the network. This is not a universal ERC-7730 validator or wallet emulator. The tool neither simulates transaction effects nor establishes semantic correctness of descriptions.
Use with an agent
The repository also includes an agent skill that drives this CLI end to end: registry search, registry add-deployment when the protocol is already listed, otherwise init from a Foundry build or a verified address, then the judgment steps (intent wording, token relationships, what to hide), fixtures, tests, export and a PR draft.
npx skills add portdeveloper/clear-signing-helperThe skill lives under .claude/skills/clear-signing-helper/ and expects the CLI installed as above. The npm package carries only the CLI; an agent reads the skill from GitHub: SKILL.md, scripts/find-in-registry.sh and scripts/verify-address.sh.
Install from source
Clone the repository, then run from its root:
npm ci --ignore-scripts
npm run build
npm install --global --ignore-scripts .
clear-signing --helpAlternatively, run node /path/to/clear-signing-helper/dist/cli.js directly after building. To create a local package, run npm pack; see distribution and checksum verification.
Forge builds have a ten-minute timeout to accommodate large optimized protocol repositories; metadata commands have a two-minute timeout.
Release status and evidence
0.5.0-preview.1 is published on npm under both the preview and latest tags, and as a GitHub prerelease with its checksum and verification record. The npm tarball's integrity hash matches the GitHub asset byte for byte. The GitHub assets contain the package checksum and local, CI, GitHub-download and npm-install verification records. All four Linux/macOS Node 22/24 CI packages matched the released tarball. See the release handoff for exact inputs and remaining distribution work.
Validation evidence records automated tests, real-source exercises and emulator results. The production readiness checklist retains pending human and physical-device validation. Those results are not prerequisites for distributing this experimental CLI. Descriptors produced with the tool are under review in the registry: #3003 (PuddleSwap StakingRewards, exported unchanged) and #2611 (Permit2 on Monad).
Licensed under MIT. Bundled dependencies retain their third-party notices.
Develop and validate
npm ci
npm test
forge test --root examples/standard
forge test --root examples/custom-layout
forge test --root examples/duplicate-contractsTests compile real Foundry projects and run the actual CLI. The Anvil test deploys a token/vault, mines a deposit, validates its on-chain result, and compares calldata with cast. Rendering tests cover tuple grouping, full signature names, numeric precision, and browser escaping.
Registry compatibility
The pinned real-registry corpus covers 278 calldata descriptors and 1,435 function entries. Every entry generates a schema-valid basic descriptor and renders all argument leaves in order. Tests also replay 605 registry transactions: 574 render; 31 are explicitly rejected because they append non-ABI suffix bytes. The tool never silently strips those suffixes.
Four deployed contract ABIs fetched from Sourcify (WETH, Uniswap Router02, Morpho Blue, Safe) are tested with their actual mutability and parameter names. Twelve representative registry ABI-shape harnesses compile through Foundry. These harnesses test integration and types, not deployed protocol logic. The 104 EIP-712 submissions are counted separately and remain outside scope.
Basic generation uses raw values; it does not reproduce curated token inference, enum labels, inner-call decoding, or other protocol-specific semantics. See the reproducible compatibility report and independent Luna verification.
To upgrade a repository initialized with v0.1:
clear-signing upgrade
clear-signing check
# Inspect the source, descriptors, and previews before accepting changes:
clear-signing review --accept
clear-signing test --updateThe PRD describes the broader intended product. Validation notes record what was actually exercised and what remains unverified.
