Skip to content

refactor(maticjs): migrate to @polygonlabs/maticjs 4.0.0-pre.2 - #102

Merged
MaximusHaximus merged 10 commits into
masterfrom
refactor/maticjs-4
Nov 10, 2026
Merged

MaximusHaximus merged 10 commits into
masterfrom
refactor/maticjs-4

Conversation

@MaximusHaximus

@MaximusHaximus MaximusHaximus commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The service builds every withdrawal proof and exit payload with @maticnetwork/maticjs 3.x and its ethers plugin. That line is frozen: future bridge fixes land only in its successor, @polygonlabs/maticjs 4.0. The 3.x plugin model also forces costs on this service:

  • a module-load use(Web3ClientPlugin) with CJS default-export workarounds, in the code and in the test config;
  • every SDK call site typed any;
  • deterministic exit errors (log not in receipt, token index out of range, not yet checkpointed) recognised by matching SDK error message text.

Solution

  • Move to @polygonlabs/maticjs, pinned exactly at the 4.0.0-pre.2 prerelease. This service is the prerelease's production dogfood before 4.0.0 is declared stable.
  • Build a typed POSClient per network from two ethersV5Adapters.
  • Keep the client cache, static providers, 10 s RPC timeout, RPC header and receipt fan-out cap (requestConcurrency: 10 → proofConcurrency: 10) unchanged.
  • block-included reads RootChain.headerBlocks through ethers directly, the SDK's documented escape hatch. Its JSON keeps the historical { "type": "BigNumber", "hex": … } fields byte-for-byte.
  • Checkpoint checks use the status read getCheckpointStatus(...).checkpointed. The 4.0 boolean predicate is gone. Same branches, same errors when the burn isn't checkpointed.
  • Deterministic exit errors are classified by the SDK condition, not message text: isMaticjsError plus LogNotFoundInReceiptError / IndexOutOfBoundsError / BurnTxNotCheckpointedError .has(). These are the SDK's own name-based recognisers: they walk the cause chain (public methods throw an operation error whose cause is the condition) and work across duplicate package copies, realms and JSON round-trips. The service never uses instanceof on SDK errors. The public 404 messages are unchanged, and every other condition, including ReceiptsRootMismatchError and TransactionNotFoundError, is retried as before. The thrown value goes to the classifier as-is, never normalised to an Error first. Tests cover each mapped condition bare and wrapped, JSON round-tripped errors, unmapped conditions and raw library errors, through the classifier, the log fields and the full exit-payload retry loop.
  • Retry-warning log lines: errorEvent is replaced by errorType (the SDK condition's legacy 3.x type, where it has one) and errorCode. errorCode is the name of the innermost SDK condition (e.g. LogNotFoundInReceiptError), or, for raw RPC failures (which 4.0 rethrows unwrapped), the RPC library's own code as before.
  • Every SDK call site that was typed any is now typed. No casts were introduced.

Known state

  • The public API is unchanged except for one edge case. All fixture responses are byte-identical to the current production service (see Verification).
  • The edge case: an exit-payload request for a burn that is still pending (submitted, not yet in a block) now gets an immediate 404 "Burn transaction has not been checkpointed yet", the same answer as for a mined burn awaiting its checkpoint. 4.0's checkpoint status reports a pending burn as "not checkpointed, reason: pending". On 3.x the checkpoint check didn't catch it and the request failed after exhausting retries, most likely a 502. A client polling until its exit is ready now gets the same 404 from submission to checkpoint. Tests pin both "not yet" reasons and the rejected-lookup path.
  • New in 4.0: buildExitPayload refuses to return a payload whose receipts don't match the block's receiptsRoot (ReceiptsRootMismatchError). That usually means an RPC served bad receipts, so the service retries it on the next RPC like any transient failure and answers 502 only after retries are exhausted.
  • Once 4.0.0 is published as stable, the exact pin moves to ^4.0.0 in a follow-up.
  • Dogfood watch (daily for 7 days after deploy):
    1. Error-level log lines by err_type, against the pre-deploy baseline (about 4 per 7 days, at most 1 per 5 minutes). Any new error type is a finding.
    2. Retry warnings by errorType/errorCode: same order of magnitude as before. Any ReceiptsRootMismatchError is a finding, recorded per RPC origin.
    3. Proof-path failures (historically Invalid merkle proof created) and their 4.0 equivalents.
    4. The service's error-rate alert and capacity alert stayed quiet, or the incident is linked here.
    5. External availability and correctness probes stay green.
    6. The byte-diff smoke against the deployed service is re-run and stays identical.
    7. The OpenAPI info.version matches the released version.
    8. No new error-tracking issue types.
    9. Day 7: dependency resolution is confirmed on the deployed tag, and the result is handed over for the 4.0.0 stable decision.

Verification

  • Battery: pnpm run lint green; pnpm test 59/59 (the 40 pre-existing tests unchanged and green, plus new tests). The test run makes live RPC calls on mainnet and Amoy, including the Bor state-sync receipt regression.
  • Byte-diff against production: the migrated service was run locally and every test-fixture route was fetched from it and from https://proof-generator.polygon.technology. Status codes and bodies are byte-identical on 25/25 routes: health, block-included (mainnet/Amoy/404/400s), fast-merkle-proof (200 + 5 × 400), exit-payload ERC-20 (with and without index, out-of-range index 404), ERC-1155, all-exit-payloads ERC-721, Amoy state-sync, and every validation 400.
  • That byte-diff was repeated against the exact 4.0.0-pre.2 tarball before it was published (25/25 identical). The published tarball's sha256 matches it, and the lockfile pins its integrity.
  • Docker: image built from a fresh frozen install; the remote-mode battery passed against the container (on the pre-publish artifact; CI's docker job covers the published one). Memory flat over 200 sequential exit-payload requests.
  • Independent review: every 3.x call and error code was checked against its 4.0 equivalent, including where the SDK throws each mapped code. No error moved between "deterministic 404" and "retried".
  • Adversarial audit: verdict safe with fixes. Two low-severity findings were fixed on this branch:
    • b419fa4: shape-based classification now happens before any Error normalisation.
    • e451b32: a full-body test pins the block-included BigNumber wire shape on an immutable header block, since the service has no response schemas to guard it.
  • The audit's SDK observations (against pre.1) are parity with 3.x, with no action:
    • POSClient.init fetches the static network index with a 1 h cache.
    • The ethers v5 adapter builds a Contract per read.
    • TRANSACTION_NOT_FOUND is retried across the RPC list before the 404.

The last release regenerated CHANGELOG.md with a heading directly above a
list, which fails markdownlint MD022 and turns the repo's lint gate red.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
…apter

The service moves off the frozen @maticnetwork/maticjs 3.x plugin stack
onto the typed 4.0 POSClient. The client cache, static providers, 10 s
timeout, eRPC header and the receipt fan-out cap (now proofConcurrency)
carry over unchanged. The raw RootChain headerBlocks read goes through
ethers directly, so block-included keeps its historical BigNumber JSON
shape byte-for-byte. Deterministic exit errors are classified on the SDK's
stable POSBridgeError code instead of message text. Retry warnings log
errorType/errorCode (SDK code, or the raw RPC library code). Every SDK
call site that was typed `any` is now typed.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
toDeterministicError is the contract between the SDK's POSBridgeError
codes and the routes' 404 responses. Cover all three mapped codes, an
unmapped code and a non-SDK error, so an SDK code rename fails here
instead of silently turning deterministic 404s into retried 502s.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
Swap the local prerelease tarball for the published 4.0.0-pre.1, pinned
exactly so the dogfood runs on one known artifact. The published tarball is
the one validated before publish: same sha256, battery 46/46, every proof
route byte-identical to production.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
An instanceof check fails whenever the error comes from a different copy of
the SDK than the one this module imported (a duplicate install, another
realm, a bundler split). A deterministic 404 would then silently become a
retried 502, and the log fields would go blank. One structural guard in
errors.ts (name 'POSBridgeError' plus a string code) now backs both the exit
error classifier and the retry-warning log fields. Tests feed error-shaped
objects that are not instances of the SDK class through both paths, with
negative controls.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
The exit-payload call sites wrapped whatever the SDK threw in `new Error(String(x))`
when it was not an Error instance, before handing it to the classifier. That
normalisation ran ahead of the shape guard, so an SDK error recognised only by its
name and code (the case the guard exists for: another copy or realm of the SDK)
lost both fields, was retried on every RPC and answered 502 instead of a
deterministic 404, with blank errorType/errorCode on the retry log. The classifier
now takes the thrown value as-is and attaches it as the cause only when it is an
Error, which VError requires; the outer retry loop still normalises for the 502
cause. A fail-fast test feeds a plain-object SDK error through the service path.

Claude-Session: https://claude.ai/code/session_01JRTxTaoqJFaPzXs5moreWN
The service has no response schema: `res.json` serialises whatever the service
returns, so the `{ type: 'BigNumber', hex }` fields consumers parse were guarded
only by an out-of-repo byte-diff against production. The SDK now returns bigint,
which JSON cannot serialise and which the raw headerBlocks read deliberately
avoids; a full-body equality on an immutable header block makes any drift in that
wire shape fail the battery instead of a consumer.

Claude-Session: https://claude.ai/code/session_01JRTxTaoqJFaPzXs5moreWN
Team test style reads object fields with .property() rather than
dereferencing them, so a missing body fails with a field-level message.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
4.0.0-pre.2 replaces the boolean checkpoint predicate with a status read,
and replaces the single coded error with named condition classes that
operation errors wrap. Library errors now arrive unwrapped. Checkpoint
checks read getCheckpointStatus().checkpointed. Deterministic exit errors
are recognised with the SDK's own name-based isMaticjsError and each
condition's has(), which walk the cause chain and work across package
copies and JSON. The public 404s are unchanged and every other condition
is still retried. Retry warnings log the innermost condition's name as
errorCode. Pinned exactly at the published 4.0.0-pre.2.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
@MaximusHaximus MaximusHaximus changed the title refactor(maticjs): migrate to @polygonlabs/maticjs 4.0.0-pre.1 refactor(maticjs): migrate to @polygonlabs/maticjs 4.0.0-pre.2 Nov 9, 2026
getCheckpointStatus answers "not yet" with a reason rather than rejecting.
Pin the two not-checkpointed reasons (a pending burn and a mined burn
awaiting its checkpoint): both answer at once with the 404 clients poll on,
without building or retrying. Also pin a rejected lookup, which is retried
on every RPC and then answers 404 "Incorrect burn transaction". The
changeset now records the pending-burn response, the one answer that
differs from before.

Claude-Session: https://claude.ai/code/session_01JRTxTYrWEMpYoCh4iv6hpv
@MaximusHaximus
MaximusHaximus marked this pull request as ready for review November 10, 2026 10:30
@MaximusHaximus
MaximusHaximus merged commit c6a373f into master Nov 10, 2026
7 checks passed
@MaximusHaximus
MaximusHaximus deleted the refactor/maticjs-4 branch November 10, 2026 10:38
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