Skip to content

Standard ERC-20 descriptor synthesis + wallet known-contracts - #3

Merged
llbartekll merged 11 commits into
mainfrom
dev
May 29, 2026
Merged

Standard ERC-20 descriptor synthesis + wallet known-contracts#3
llbartekll merged 11 commits into
mainfrom
dev

Conversation

@llbartekll

Copy link
Copy Markdown
Owner

Summary

  • Resolver synthesizes a single-selector ERC-7730 descriptor in memory for transfer / approve / transferFrom when the wallet's DataProvider returns token metadata for the contract; short-circuits the registry lookup. Proxy contracts split descriptor matching (implementation_address) from token lookup (tx.to).
  • Synth descriptors render Unlimited for approve(amount = 2^256 - 1) (via threshold + message), and Sender for addressName fields where the address equals tx.from (via senderAddress: "@.from"). Scoping is tightened so only approve.amount carries the Unlimited threshold — transfer / transferFrom still render the full decimal at uint256.max.
  • Wallet gains a SeedContractStore mirroring SeedTokenStore, 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.resolveLocalName consults it after the user-self check, gated on types == nil || types.contains("contract") so ENS reverse-resolution stays in scope when synth descriptors leave the hint unset.

Note: dev is diverged from origin/main (4 commits ahead, 10 behind). Expect merge conflicts to resolve in the PR.

Test plan

  • cargo test -p clear-signing --features uniffi,github-registry (49 unit + 101 integration, plus new standard_token + standard_token_e2e suites)
  • cargo clippy -p clear-signing --all-targets --features uniffi,github-registry -- -D warnings
  • WalletTests xcodebuild on iPhone 17 Pro Simulator (22 tests, incl. 5 new SeedContractStore cases + bundle canary)
  • Manual: 1inch-on-Optimism approve(USDT, max) renders "Approve Aave V3 Pool to spend Unlimited USDT"

🤖 Generated with Claude Code

llbartekll and others added 11 commits May 25, 2026 12:33
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
llbartekll merged commit 89f49cf into main May 29, 2026
3 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant