Skip to content

feat(extension): thread chainId through threat API lookups (S-16)#6

Merged
Lykhoyda merged 1 commit intomainfrom
feat/api-chainid-sync
Apr 20, 2026
Merged

feat(extension): thread chainId through threat API lookups (S-16)#6
Lykhoyda merged 1 commit intomainfrom
feat/api-chainid-sync

Conversation

@Lykhoyda
Copy link
Copy Markdown
Owner

Summary

Unblocks the testudo-api deploy. API PR #5 moves client-facing threat routes under /api/v1/chains/:chainId/... — without this change, deploying produces 404s on every threat lookup. Ship order: this lands in Chrome Web Store before the API rolls out.

What changed

api-client.ts

  • ApiClientOptions.chainId?: number
  • checkAddressThreat() + pingApi() build URLs as /api/v1/chains/${chainId}/threats/address/...
  • resolveChainId() falls back to 1 for missing / non-finite / non-positive input (matches api-server behavior; keeps cold-start protection alive)
  • ThreatResponse exposes optional matchedChainId + crossChainMatch (reserved for future cross-chain UX)
  • checkDomainThreat() unchanged — domain route stays global

analysis.ts

  • AnalysisDeps.checkAddressThreat accepts {chainId?}
  • analyzeWithCache() / addressCheckWithCache() accept optional chainId
  • Cache key is now ${chainId}:${address} — same address on different chains caches independently. pendingFullAnalysis / pendingAddressCheck use the same key for request coalescing

injected.tsx

  • Lazy getCurrentChainId() via eth_chainId RPC; cached, invalidated on wallet's chainChanged event
  • parseChainIdHex() accepts number or 0x-hex; rejects 0 / NaN / invalid
  • Permit + typed-data flows prefer the typed domain's chainId, fall back to active chain
  • EIP-7702 uses the auth's chainId; when that is 0 (replay risk) the threat lookup falls back to the active chain so we key into a real chain's registry instead of a sentinel

Message bridge

  • content.ts, services/messaging.ts, background.ts forward chainId end-to-end. Handlers coerce non-number inputs to undefined so the api-client default applies at the URL boundary

Tests — 375 pass (9 new)

api-client.test.ts

  • URL has chains segment
  • Custom chainId routed to correct path
  • Default to chainId=1 on missing / 0 / -1 / NaN
  • pingApi routing + default

analysis.test.ts

  • chainId forwarded to checkAddressThreat (address-only + full pipeline)
  • Per-chain cache partitioning
  • Default cache key uses chain 1
  • Cross-chain cache isolation

Test plan

  • yarn workspace @testudo/extension run test375 pass
  • yarn workspace @testudo/extension run build — clean (injected.js 83.2 KB)
  • yarn biome check packages/extension/src packages/extension/tests — clean
  • E2E on CI (happens on PR open)
  • Manual QA: switch chains in wallet, trigger an approval — confirm URL includes new chain id (via devtools / API logs)

Ship sequence

  1. Merge this PR
  2. Submit extension to Chrome Web Store (24–72h review)
  3. After extension rollout in production, deploy testudo-api PR fix(core+injected): replay-risk typed confirm + core stability hardening #5

🤖 Generated with Claude Code

testudo-api PR #5 moves client-facing threat routes under
`/api/v1/chains/:chainId/...`. Without this change, deploying the API
produces 404s on every threat lookup. Ship order: extension lands in
Chrome Web Store -> then API deploys.

## api-client.ts
- `ApiClientOptions.chainId?: number`; `checkAddressThreat()` and
  `pingApi()` build URLs as `/api/v1/chains/${chainId}/threats/address/...`
- `resolveChainId()` falls back to `1` (mainnet) for missing/non-finite
  /non-positive input — matches api-server's current behavior for
  missing chainId and keeps cold-start protection alive
- `ThreatResponse` adds optional `matchedChainId` + `crossChainMatch`
  (ignored in UI for now, reserved for future cross-chain surfacing)
- `checkDomainThreat()` unchanged — domain route stays global

## analysis.ts
- `AnalysisDeps.checkAddressThreat` accepts `{chainId?}`
- `analyzeWithCache()` / `addressCheckWithCache()` accept optional `chainId`
- Threaded through `performThreeLayerAnalysis` + `performAddressOnlyCheck`
- Cache key is now `${chainId}:${address}` — same address on different
  chains caches independently. `pendingFullAnalysis` /
  `pendingAddressCheck` use the same key for coalescing

## injected.tsx
- Lazy `getCurrentChainId()` via `eth_chainId` RPC, cached in-module,
  invalidated on wallet's `chainChanged` event
- `parseChainIdHex()` accepts number or 0x-hex; rejects 0/NaN/invalid
- Permit + typed-data flows prefer the typed domain's chainId,
  fall back to active chain
- EIP-7702 authorization uses the auth's chainId; when that is `0`
  (replay risk) the threat lookup falls back to the active chain so
  we still key into a real chain's registry instead of a sentinel

## background.ts + content.ts + services/messaging.ts
- chainId is forwarded end-to-end through the channel and runtime
  message bridge. Handlers treat non-number values as `undefined`
  so the api-client default applies at the URL boundary

## Tests (9 new, 375 total)
- api-client: URL has chains segment, custom chainId routed, default
  to 1 on missing / negative / NaN, pingApi routing, pingApi default
- analysis: chainId forwarded to checkAddressThreat for both flows,
  per-chain cache partitioning, default cache key, cross-chain cache
  isolation
- Existing tests updated to assert the new cache-key shape

Build + lint clean. No behavior change when chainId is omitted —
every path defaults to mainnet (1).
@Lykhoyda Lykhoyda merged commit cb0ba0d into main Apr 20, 2026
3 checks 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.

1 participant