Skip to content

feat(identity): one-call registry reads, a viem-free handle export, and the ENS Parent Name - #86

Open
SupremaLex wants to merge 11 commits into
mainfrom
feat/registry-views
Open

SupremaLex wants to merge 11 commits into
mainfrom
feat/registry-views

Conversation

@SupremaLex

@SupremaLex SupremaLex commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Five read functions on IdentityRegistry, a viem-free handle export in @libid/contracts, the ENS Parent Name as a constant, and a NatSpec pointer fix. This PR offers a menu: keep the reads that are worth keeping and drop the rest.

The reads

All five are on IdentityRegistry and IIdentityRegistry. Storage is unchanged; both storage-layout checks pass.

Function Returns Why
handleBindingOf(platformId, handle) (holder, observedAt) A holder and its proof age in one call, with resolveHandle's semantics. LibID (libID #71) needs this. Today it infers state from which registry call reverts.
idBindingOf(platformId, id) (holder, observedAt) resolveId plus the proof age.
handleOfId(platformId, id) (handle, current) An account's latest handle and whether it still holds it, without an indexer.
idOfHandle(platformId, handle) id Which account holds a handle now, so a client can see a handle change hands without remembering the previous id.
normalizeHandle(platformId, handle) normalized The handle under this chain's current rules, so clients (@libid/ens, the ENS gateway) can use the chain's rules instead of copies made at release time.

Semantics:

  • Platform gate. The first four revert UnknownPlatform on an unwired platform, as resolveHandle and resolveId do. normalizeHandle follows handleHashOf: it answers for a platform with rules but no verifier, and reverts UnusableHandle(problem) on text the rules refuse.
  • Refused text. In the other reads it answers (0, 0) or "", not a revert.
  • A retired handle. handleBindingOf gives (0, old observedAt), as handleBinding does. That is the watermark a new proof has to beat.
  • A handle its identity renamed away from. idOfHandle returns "" until another identity proves it, matching resolveHandle's zero.
  • One code path each. resolveHandle and resolveId now go through handleBindingOf and idBindingOf. identitiesOf and handleOfId share one _handleOf. handleHashOf hashes normalizeHandle's result. No existing ABI changes.

TS

  • @libid/contracts/handle: normalize, rulesFor, the rules, the handle vectors and ENS_PARENT_NAME, with no viem at load time. A test walks the subpath's imports and fails on any import that isn't relative, viem included. ./identity still needs viem. This lets @libid/ens (libID #109) drop its viem peer.
  • ENS_PARENT_NAME = 'handles.link': also exported from ./identity and the root. The doc comment says the testnet deployment uses testnet.handles.link.
  • Typed reads: every resolve.ts wrapper now takes its function name, arguments and return type from the generated identityRegistryAbi. A misspelled name or a wrong tuple fails tsc. The tests for the four new wrappers decode with the real ABI.
  • No TS normalizeHandle wrapper: it would send the handle text to the RPC. The README points to normalize(handle, await rulesOf(...)), which runs locally. The contract view and its Rust binding stay.

For review

  • Interface. The five reads live in IIdentityRegistry, which already carries reads (handleBinding, handleHashOf, …) and now describes itself as the registry's reads for contracts and clients alike. The escrow tests' SettableRegistry stubs revert ("not modelled"), so they can't give results the real registry never would.
  • handleOfId.current after rules narrow. It stays true while resolveHandle, handleBindingOf and idOfHandle answer zero for that handle. That matches the existing identitiesOf.handleCurrent (pinned by test_handleCurrentReadsTheNodesNotTheRules) and is documented in the NatSpec, the Rust docs and the TS type.
  • Names. idOfHandle is live: it answers only while the handle has a holder, unlike the raw idNodeByHandle pointer an indexer may mirror. The NatSpec says so.
  • Bindings older than the preimage maps. On a proxy holding bindings written before idPreimages and handlePreimages existed, handleOfId and idOfHandle return empty for those bindings, the same gap identitiesOf has.
  • Gas on existing views. Measured in one probe transaction against main: resolveId and resolveHandle cost about 110–150 gas more, because they run through the new reads, one code path kept on purpose. handleHashOf and handleNodeOf cost 44–88 more, and the five new selectors add about 22 to dispatch. identitiesOf costs about 74 more per item. Restoring main's exact loop body measured worse, so the cause looks like compiler output for the extra string-returning functions rather than this source. The committed verifier gas snapshots are unchanged.
  • HandleResolver NatSpec. It cites libID's ENS integration spec (specs/ens-integration.md, REQ-ENS-KEY-01) without a PR number. It states the three-way key split, that the current deployment does not meet it yet, and that resolvers sharing a Signer must sit at different addresses (REQ-ENS-KEY-02).

Checks

  • Solidity:
    • forge build, and forge test 673/673, including 22 new tests: bound, unbound, refused and unwired cases, rename and takeover, two identities of one holder, platforms kept apart, rules that narrow, case and @ variants, and agreement with identitiesOf.
    • forge fmt --check; both storage-layout scripts.
    • The regen-*.py --check scripts; gas snapshots unchanged.
  • Rust: cargo clippy -D warnings, and cargo test --all including the binding drift test and the anvil tests. cargo +nightly fmt --check ran on a local nightly, not CI's pin.
  • TS: install, codegen, build, typecheck, 43/43 tests including the viem-free guard, lint, fmt.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ar21wPcHzNprswTzLJd3MA

Add five views to IdentityRegistry and IIdentityRegistry:

- handleBindingOf(platformId, handle) -> (holder, observedAt): resolveHandle's
  reading (normalized on chain, refused text answers (0, 0), unwired platform
  reverts UnknownPlatform) plus the binding's observedAt. A retired handle
  answers a zero holder beside its watermark, as handleBinding does.
- idBindingOf(platformId, id) -> (holder, observedAt): resolveId plus
  observedAt.
- handleOfId(platformId, id) -> (handle, current): the handle the identity
  proved most recently and identitiesOf's handleCurrent for it; ("", false)
  for an id never proved.
- idOfHandle(platformId, handle) -> id: the id of the identity the handle
  belongs to now; empty when nobody holds it (never proved, or retired by a
  rename) and for refused text.
- normalizeHandle(platformId, handle) -> normalized: joins the rules views
  (rulesOf, handleHashOf, handleNodeOf): needs a configured platform only and
  reverts UnusableHandle for refused text, as handleHashOf does.

resolveId and resolveHandle now read through idBindingOf and handleBindingOf,
handleHashOf hashes normalizeHandle's answer, and identitiesOf and handleOfId
share one helper for the latest handle and its current flag. No storage
change; the layout snapshot still matches.

The Rust sol! bindings and the TS resolve helpers (idBindingOf,
handleBindingOf, handleOfId, idOfHandle, normalizeHandle) expose the same
reads. SettableRegistry, the escrow tests' registry mock, implements the
widened interface.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
`@libid/contracts/identity` re-exports the resolvers and node hashing, which
import viem when the module loads, so a consumer that only normalizes a
handle had to install viem. The new `./handle` export carries handle.ts and
handleVectors.ts alone: normalize, rulesFor, Rules, RULES_*, HandleError,
the PLATFORM_*_KEY constants and the generated vector table. `./identity`
keeps exporting all of it.

Checked from the packed tarball installed alone in an empty directory:
`import('@libid/contracts/handle')` loads and normalizes, while
`@libid/contracts/identity` fails with ERR_MODULE_NOT_FOUND for viem.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
ENS_PARENT_NAME is `handles.link`, the name scripts/setup-ens-resolver.sh
and the deploy workflow set HandleResolver on in production. The testnet
deployment uses `testnet.handles.link` on Sepolia (deploy.yml). Exported
from `@libid/contracts/identity` and the root; the Rust crate keeps no ENS
constants, so it has no counterpart there.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
libID PR #93 moves design/ens-integration.md to specs/ens-integration.md;
the NatSpec now cites the spec and REQ-ENS-KEY-01. The existing rule against
sharing a signing key between deployments that could land on one address
agrees with REQ-ENS-KEY-02 and is unchanged.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
- IIdentityRegistry describes itself as the registry's reads, for contracts
  and clients alike, rather than only what other contracts ask.
- Its NatSpec for handleOfId and idOfHandle, and the Rust sol! docs, state
  that both revert UnknownPlatform for an unwired platform.
- SettableRegistry's stubs for the five reads revert "not modelled" instead
  of answering made-up values a later escrow test could come to rely on.
- resolveHandleAndId reads its handle node through _usableHandleNode, the
  gate the other handle reads share.
- handleOfId documents that `current` reads the nodes and stays true after
  the rules narrow while resolveHandle, handleBindingOf and idOfHandle answer
  nobody; idOfHandle and handleOfId note how they differ from the raw
  handleNodeById/idNodeByHandle pointers an indexer may mirror under similar
  names. The names stay: idOfHandle is documented as live, and handleOfId
  returns the pointer's handle with `current` beside it.
- Tests: handleBindingOf keeps platforms apart for `alice`, valid on X and
  GitHub; idOfHandle answers empty after the rules narrow, with
  resolveHandle and handleBindingOf, while handleOfId stays current.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
- The resolvers' read helper takes its function name, arguments and result
  type from the generated identityRegistryAbi (viem's ContractFunctionName,
  ContractFunctionArgs and ContractFunctionReturnType), so a misspelled
  name or a result read in the wrong shape fails to compile. Every wrapper
  in resolve.ts uses it.
- Tests for idBindingOf, handleBindingOf, handleOfId and idOfHandle run
  through a real viem PublicClient whose transport answers eth_call with
  results encoded by the ABI and decodes the calldata it receives, and they
  assert UnknownPlatform by name on a decoded revert.
- The normalizeHandle wrapper is gone: it sent the handle to an RPC, and
  `normalize(handle, await rulesOf(reader, x))` gives the same answer
  locally. The README says so, and that rulesOf needs `/identity` (viem and
  an RPC).
- HandleOfId.current and the README document that it reads the nodes and
  stays true after the rules narrow while handleBindingOf and idOfHandle
  answer null.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
`@libid/contracts/handle` re-exports ENS_PARENT_NAME, which imports
nothing. A test walks the subpath's relative imports from handle/index.ts
and fails on any package specifier, viem included, and on any module
joining the set it loads (handle/index, identity/ens, identity/handle,
identity/handleVectors). Adding a viem import to identity/ens.ts makes it
fail. Checked again from the packed tarball installed alone: the subpath
loads and exports `handles.link`.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
Cite libID's ENS integration spec by path, with no pull request number, and
state what REQ-ENS-KEY-01 and REQ-ENS-KEY-02 require: the DNS registrar
account, the Parent Name and resolver owner, and each Signer are distinct
keys, and resolvers trusting one Signer sit at different addresses on
whichever chains. The spec's note that the current deployment does not
meet KEY-01 replaces the earlier wording about one identity.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The one-call read tests repeated the UnknownPlatform block for the four
resolvers, the UnusableHandle encoding four times, and the X-without-
underscores setup already used by the handleCurrent test. Each is one
helper now. test_handleOfIdReadsTheNodesNotTheRules is dropped:
test_idOfHandleFollowsTheRulesWhenTheyNarrow makes the same assertions
after the same setup.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
…ve.ts

Five reads turned the zero address into null with the same ternary; they
share `orNull`. The ABI return type `read` spelled twice is one alias.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
The paragraph that described `normalize(handle, await rulesOf(...))` is
a snippet with its import now, and the note on `current` under narrowed
rules is one line; the full rule stays in `HandleOfId`'s doc comment.

Assisted-by: Claude Opus 5.5
Signed-off-by: SupremaLex <georglutsenko@gmail.com>
@SupremaLex
SupremaLex marked this pull request as ready for review October 6, 2026 08:07
@SupremaLex
SupremaLex requested a review from xgreenx October 6, 2026 08:07

This branch has not been deployed

No deployments
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