Standard ERC-20 descriptor synthesis + wallet known-contracts - #3
Merged
Conversation
When the calldata selector is transfer/approve/transferFrom and the wallet's DataProvider returns token metadata for the contract, the resolver synthesizes a single-selector descriptor in memory and skips the registry lookup. Strict short-circuit: matched cases never consult the source, even if it would have returned a richer descriptor. Trust signal stays delegated to the wallet via resolve_token. Proxy contracts split descriptor matching (implementation_address) from token lookup (user-facing tx.to). FFI exported symbols are unchanged -- wallets already implementing resolve_token for format_calldata get the synth path for free. Includes integration tests locking the short-circuit policy, nested Safe execTransaction + EIP-712 wrapping, a proxy-caller path, and an end-to-end fixture harness with seed fixtures + a manual Etherscan generator example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two ERC-7730 spec-defined edge cases the synthesized descriptors didn't
cover, both visible on real wallet flows:
1. amount = 2^256 - 1 (the DeFi infinite-approval sentinel) used to
render as the full 70-digit decimal expansion. tokenAmount params
now carry threshold + message "Unlimited", so the engine renders
"Approve {spender} to spend Unlimited USDT" -- matching what a
hand-written descriptor for the same function would produce.
2. addressName fields where the address equals tx.from now carry
senderAddress: "@.from". Engine renders "Sender" for those, common
in delegated transferFrom flows and self-transfers.
Adds two unit tests asserting both params are set on every synth, three
integration tests pinning the rendered output through format_calldata
(including a "max - 1" test that locks the >= boundary), and a fixture
matching the exact 1inch-on-Optimism approve from the bug report.
Revoke detection for approve(spender, 0) considered and deferred: spec
doesn't support conditional intent in a single descriptor entry; the
override belongs in the wallet's display layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bug-report flow now renders "Approve 0x794a...4814aD to spend
Unlimited USDT". That address is Aave V3 Pool on Optimism (verified in
the EF 7730 registry). The library's addressName chain already supports
this end-to-end -- it was just not populated wallet-side.
Wallet:
- Add SeedContractStore (mirrors SeedTokenStore) reading a bundled
known-contracts.json keyed by eip155:{chain}/contract:{addr}.
- Seed ~30 entries copied verbatim from the EF registry covering the
wallet's 5 supported chains: Aave V3 Pool, 1inch v5/v6 routers,
ParaSwap Augustus V6.2, Permit2, Safe singleton 1.4.1.
- Extend WalletMetadataProvider.resolveLocalName: user-self check
(preserved) -> known-contracts lookup -> nil. Always-check by
contract regardless of the types hint; small enough to stay cheap.
- Add LookupKey.contract and ContractMetadata next to the existing
token helper.
- 5 new XCTests cover known-contract hit, multi-chain keying,
wallet-self precedence, unknown-returns-nil, and case-insensitive
lookup. All 22 WalletTests pass on iPhone 17 Pro Simulator.
- Wire SeedContractStore.swift into both Wallet and WalletTests Sources
build phases; bundle known-contracts.json via a new Resources group.
Library:
- standard_token synth now sets the spec-defined `types` hint on
addressName params: ["contract"] on approve.spender (unambiguous),
None on transfer.to / transferFrom.from / .to (caller checks all
sources). Engine passes it through to resolveLocalName so wallets
can route lookups by role; today it's a future-proofing hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Only approve.amount carries threshold+message; transfer/transferFrom
render the full decimal at uint256.max.
- Synth drops the unilateral addressName.types=["contract"] hint on
approve.spender so wallets keep ENS reverse-resolution in scope.
- Wallet resolveLocalName now gates the bundled contract-store lookup
on types == nil || types.contains("contract") (case-normalized).
- Bundle known-contracts.json into the WalletTests target and add a
canary that asserts a known entry decodes from the test bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clippy::manual_is_multiple_of fires on the new ERC-20 fixture example and e2e test under Rust 1.95+; matches the same cleanup applied to parity checks on main in 51cd5b9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UniFFI's Kotlin generator emits each variant of a thrown sealed-class enum as a nested class extending kotlin.Exception, which exposes a Throwable.message property. A variant field literally named `message` collides with that inherited property, so the generated clear_signing.kt failed to compile and the Android CI job had been red since the FormatFailure type was introduced. Renaming the wire-level field to `detail` removes the collision. The public Swift API stays source-compatible via the existing extension: `var message: String` on FormatFailure now reads from `detail`. The Kotlin wrapper exposes the same surface via `failureMessage`. Updates the React Native demo + regenerated TS bindings accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pom.withXml block appended a second top-level <dependencies> element to the release POM. Maven only allows one, so publishReleasePublicationToMavenLocal failed validation and the Android CI job stayed red even after the FormatFailure Kotlin fix unblocked the compile step. `from components.release` already emits JNA with <type>aar</type> from `api 'net.java.dev.jna:jna:5.17.0@aar'`. The manual entry was also adding <classifier>android</classifier>, but JNA does not publish an android-classified artifact — the AGP-emitted entry is the correct one. Verified locally that generatePomFileForReleasePublication now produces a single, valid <dependencies> block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`findProperty('version')` returned Gradle's default project version
("unspecified") rather than null, so the previous `?: '0.0.0'`
fallback never triggered. The library was being published as
`com.github.llbartekll:clear-signing:unspecified` while the
consumer-smoke app looks up `clear-signing:0.0.0`, so its
debugRuntimeClasspath resolution failed and the Android CI job
stayed red after the POM duplication fix.
Switching to `clearSigningVersion` (the property the consumer
already reads from its gradle.properties) lets both ends agree on
the default without a CLI override, and still allows the release
workflow to pass an explicit version when needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect the renamed wire-level field on FormatFailure variants (message -> detail) across the Kotlin, Swift, and React Native integration guides: - Variant signatures now show `detail` instead of `message`. - Kotlin example switched to `failure.failureMessage` (the extension returns the original detail; `Throwable.message` carries a formatted summary). - Swift example destructures the case's `detail` and notes the ClearSigningClient extension exposes `var message`/`var retryable` for source compatibility. - React Native example reads `e.inner.detail`. Adds a note to the Kotlin local-publish steps documenting the clearSigningVersion property (default 0.0.0, matches the consumer smoke app). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`coerceFailure` was still passing the `message:` argument label, which would break the Wallet iOS build after the FormatFailure variant field rename. Swift only flagged this once the renamed binding regenerated locally — the iOS app is not on CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
llbartekll
added a commit
that referenced
this pull request
Jun 4, 2026
Resolve library-side output mismatches surfaced by the upstream registry's v2 test migration (manuelwedler/clear-signing-erc7730-registry #2/#3/#4): - date: RFC-3339 "Z" suffix instead of " UTC" - token amounts: zero renders "0" (not "0.0") - addresses: EIP-55 checksum on the EIP-712 and raw-format paths (calldata parity) - array-of-struct scopes: element-major ordering (fixes wrong field labels) - interpolatedIntent: match {name} templates against #.-prefixed field paths - amount format: render as a native-currency amount - unit base: resolve $.metadata.constants.* references Add cs-test regression fixtures (degate, lido, uniswap, aave, yieldxyz) vendored from the registry. The nested-call case (kiln) is #[ignore]'d: the runner resolves only the outer descriptor, a harness gap rather than an engine bug. The aave Borrow case is dropped (its @.from field is unavailable to the runner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
transfer/approve/transferFromwhen the wallet'sDataProviderreturns token metadata for the contract; short-circuits the registry lookup. Proxy contracts split descriptor matching (implementation_address) from token lookup (tx.to).Unlimitedforapprove(amount = 2^256 - 1)(viathreshold+message), andSenderforaddressNamefields where the address equalstx.from(viasenderAddress: "@.from"). Scoping is tightened so onlyapprove.amountcarries the Unlimited threshold —transfer/transferFromstill render the full decimal at uint256.max.SeedContractStoremirroringSeedTokenStore, seeded with ~30 verified entries from the EF 7730 registry (Aave V3 Pool, 1inch v5/v6, ParaSwap Augustus V6.2, Permit2, Safe 1.4.1) across the wallet's 5 supported chains.WalletMetadataProvider.resolveLocalNameconsults it after the user-self check, gated ontypes == nil || types.contains("contract")so ENS reverse-resolution stays in scope when synth descriptors leave the hint unset.Test plan
cargo test -p clear-signing --features uniffi,github-registry(49 unit + 101 integration, plus newstandard_token+standard_token_e2esuites)cargo clippy -p clear-signing --all-targets --features uniffi,github-registry -- -D warningsWalletTestsxcodebuild on iPhone 17 Pro Simulator (22 tests, incl. 5 newSeedContractStorecases + bundle canary)approve(USDT, max)renders "Approve Aave V3 Pool to spend Unlimited USDT"🤖 Generated with Claude Code