Skip to content

feat(utils): add EIP-55 checksum support to normaliseAddress/validateAddress (Closes #394)#407

Merged
Lakes41 merged 1 commit into
Adamantine-guild:mainfrom
XxHugheadxX:feat/394-eip55-checksum
Jul 26, 2026
Merged

feat(utils): add EIP-55 checksum support to normaliseAddress/validateAddress (Closes #394)#407
Lakes41 merged 1 commit into
Adamantine-guild:mainfrom
XxHugheadxX:feat/394-eip55-checksum

Conversation

@XxHugheadxX

Copy link
Copy Markdown
Contributor

Closes #394.

normaliseAddress gains an opt-in { checksum } option, and validateAddress now
verifies the EIP-55 checksum automatically for mixed-case input. No new runtime
dependency — this reuses the js-sha3 keccak-256 already backing toChecksumAddress.

The design decision worth explaining

The default of normaliseAddress stays lowercase, deliberately. Lowercase is this
SDK's canonical internal form: GuildPassClient builds cache keys through
normaliseAddress in six places, and areAddressesEqual compares through it. Emitting
mixed case by default would split cache entries for a single wallet across casings and
silently break prefix-based invalidation. So the checksummed form is opt-in on the
normaliser, while validateAddress — a different surface, one that already rejects
things — gets the automatic check.

Detection follows the intent of EIP-55: only a mixed-case payload carries checksum
information. An all-lowercase or all-uppercase address carries none, so it is accepted
exactly as before and the common lowercase path is unaffected. { strict: true } is
unchanged and still forces the check on any casing.

normaliseAddress(addr, { checksum: true }) on a malformed string returns the lowercase
form rather than a meaningless checksum — toChecksumAddress does not validate its
input, and this function has never thrown. Rejecting bad input remains validateAddress's job.

Test fixtures changed (and why)

Three test files pinned 0xAbCdEf1234567890AbCdEf1234567890AbCdEf12 as their "mixed
case" wallet — an address that is mixed case but whose EIP-55 checksum does not check
out. It reached validateAddress through AccessService.checkAccess,
RolesService.getUserRoles and RolesService.hasRole, so the new verification
correctly rejected it and 17 tests failed.

Those tests are about lowercase normalisation in URLs, not about checksums, and every
assertion derives from the fixture via .toLowerCase(). I replaced the literal with
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 — mixed case, correctly checksummed, and
already used elsewhere in the suite — so they keep testing exactly what they tested
before, on a realistic address.

Tests

tests/validation.test.ts gains the four canonical EIP-55 vectors from the spec, each
exercised in checksummed, all-lowercase and all-uppercase form, plus corrupted
mixed-case variants (one character's case flipped) that must be rejected. The corruption
cases assert the fixture is still mixed case first, so a rejection cannot pass for the
wrong reason. There is also a cache-key regression test pinning that every casing of a
wallet normalises to one key, and coverage for the malformed-input fallback and the
INVALID_ADDRESS / reason: 'checksum_failed' error shape.

Full suite: 1314 passing. Two unrelated tests (siwe.test.ts, security/verify-response.test.ts)
hit the 5s default timeout on my machine and pass at --testTimeout=30000;
api-report-diff.test.ts does not load because it imports scripts/lib/api-report-diff.mjs,
which is absent from main. pnpm build is green.

Also included

api-report/guildpass-sdk.api.md is regenerated, since normaliseAddress's public
signature changed (+3/-1 lines). Docs updated: sdk-guide.md, a new "Address Utilities"
section in api-reference.md, the THREAT_MODEL.md row that documented the checksum as
strict-only, and CHANGELOG.md.

Left untouched, on purpose

toChecksumAddress hardcodes a special case for one address. I verified it is redundant —
the generic algorithm produces the identical correct output for it and for all four
canonical vectors — but removing it is #175, not this issue. The duplicate
toChecksumAddress in src/crypto/secp256k1.ts is likewise left alone.

normaliseAddress gains an opt-in { checksum } option. The default stays
lowercase: GuildPassClient derives cache keys through it, so emitting mixed
case by default would split entries for one wallet across casings.

validateAddress now verifies the checksum whenever the hex payload is mixed
case, since only mixed case carries checksum information. All-lowercase and
all-uppercase input is accepted as before; { strict } is unchanged.

Adds the canonical EIP-55 vectors and corrupted mixed-case variants. Three
fixtures used a mixed-case address with an invalid checksum and were swapped
for a correctly checksummed one. Regenerates the API report.

Closes Adamantine-guild#394
@Lakes41
Lakes41 merged commit 8abb570 into Adamantine-guild:main Jul 26, 2026
1 check passed
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.

Add EIP-55 checksum support to normaliseAddress/validateAddress

2 participants