refactor: rewrite & modernization - @polygonlabs/pos-sdk@1.0.0 #476
Draft
MaximusHaximus wants to merge 3 commits into
Draft
refactor: rewrite & modernization - @polygonlabs/pos-sdk@1.0.0 #476MaximusHaximus wants to merge 3 commits into
MaximusHaximus wants to merge 3 commits into
Conversation
| # is not blown by checkpoint waits. | ||
| POS_SDK_TEST_E2E_ENABLED: 'true' | ||
| steps: | ||
| - uses: 0xPolygon/pipelines/.github/actions/ci@main |
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
2 times, most recently
from
May 1, 2026 14:57
e410c24 to
a4870f3
Compare
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
June 3, 2026 18:50
a4870f3 to
d0d0b5f
Compare
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
June 3, 2026 23:43
d0d0b5f to
df65838
Compare
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
2 times, most recently
from
July 1, 2026 14:25
4162952 to
bf978ef
Compare
…e 1.0 monorepo Workspace-level scaffolding for the @polygonlabs/pos-sdk 1.0 rewrite; no package source. Wires up the build/lint/test/packaging gates the per-package commits land on top of. Build / lint plumbing - tsconfig.json: project reference to packages/pos-sdk/tsconfig.build.json. - pnpm-workspace.yaml: publicHoistPattern @tsconfig/* so tsup's load-tsconfig (resolving from node_modules/.pnpm/...) can follow `extends: "@tsconfig/node20"`. - package.json: @tsconfig/node20 devDep; root `test` script (`pnpm -r --if-present run test`) so the shared CI action actually executes the workspace test suites — previously it silently no-op'd and "CI green" meant lint+typecheck only. - eslint.config.js: prunes stale maticjs ignore patterns; adds a path-scoped `no-restricted-syntax` banning `as unknown as` double-assertions across packages/pos-sdk/src/** with src/adapters/** exempted (the sanctioned viem/ethers boundary). - .prettierignore: packages/pos-sdk/src/abi/** — codegenned modules the drift gate compares against raw codegen output; formatting them would fail the gate on a false positive. - .gitattributes: marks the codegenned ABI modules linguist-generated so they collapse in PR diffs. CI - ci-trigger.yml: forwards POS_SDK_TEST_* secrets via job.env; builds @polygonlabs/pos-sdk on Node 20/22/24 and runs the packed-tarball smoke test (test:pack) on each matrix leg; new browser-smoke job builds the test-app bundle with Vite (itself the no-polyfill gate) and runs the Playwright suite in real Chromium. - ci-nightly.yml (new): runs the gated e2e cycle daily. - codegen-drift-check-trigger.yml (new): thin trigger for the shared apps-codegen-drift-check workflow — regenerates the ABIs from @polygonlabs/meta and fails on any diff, so the committed modules cannot silently drift from their source. - dependabot.yml (new): weekly npm + github-actions updates. The @polygonlabs/meta devDep is deliberately excluded from the grouped batch — a meta release changes the generated ABIs, so it arrives as its own PR where the drift gate and test suite review it. Removes the stale PLAN.md stub from the repo root — internal working documents don't ship on the public default branch.
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
July 8, 2026 21:16
70855dc to
570b99e
Compare
| export async function httpGet<T>(url: string): Promise<T> { | ||
| let res: Response; | ||
| try { | ||
| res = await fetch(url, { |
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
July 16, 2026 23:23
570b99e to
6123526
Compare
…-sdk 1.0
Ground-up rewrite of the Polygon PoS bridge SDK. Renames the package,
removes the plugin layer, dismantles the BaseToken inheritance chain in
favour of composition, and re-bases the whole surface on modern,
cross-environment, statically-analysable primitives. zkEVM is out of
scope — those consumers stay on @maticnetwork/maticjs (which remains
published, in maintenance mode) until the chain winds down.
Why
- The plugin model mutated module globals (utils.Web3Client = X),
making multi-tenant use unsafe — a production hazard.
- The lazy ITransactionWriteResult conflated submitted vs confirmed;
a caller assuming getTransactionHash() was idempotent caused a
production double-broadcast.
- BaseToken → POSToken → ERC20 forced non-token contracts to extend a
token base, blocking new wrappers.
- ABIs loaded from a single CDN at runtime — a global SPOF.
- BaseBigNumber predated native bigint; 27 `: any` + 50 `as` casts gave
consumers no compile-time safety.
Construction — per-library adapter factories (static, tree-shakeable)
- Consumers wrap their viem / ethers v5 / ethers v6 client with a
factory imported from a subpath (`viemAdapter` from
@polygonlabs/pos-sdk/viem, `ethersV5Adapter` from /ethers-v5,
`ethersV6Adapter` from /ethers-v6) and pass it as parent/child. The
main entry imports no web3 library; you ship only the one you use.
No plugin, no global state, no dynamic imports — fully static.
- WriteRequest.chainId is enforced as a pre-broadcast cross-chain
mis-send guard uniformly in all three adapters.
- TxResult = { hash, confirmed() }; confirmed() memoises the receipt
wait but evicts on rejection, so a transient RPC failure never
permanently poisons the result.
Cross-environment — genuinely, and gated
- Zero `Buffer`, zero `node:*`, zero dynamic import, zero Node-only
transitives: the @ethereumjs v5-line (block/common/trie/util) is
replaced by @ethereumjs/mpt v10 (trie ops verified byte-identical),
which drops the events/buffer/readable-stream chains that previously
forced browser consumers to polyfill. setLengthLeft is inlined into
utils/bytes.ts; the sole remaining @ethereumjs dependency is the
merkle-patricia trie.
- The Vite browser bundle of the test-app builds with NO stubs and NO
polyfills — the build itself now fails if a Node builtin re-enters
the graph.
Addresses — resolved from the same live index 3.9.x reads
- POSClient.init fetches the SAME nested CDN index files the legacy SDK
consumes (network/mainnet/v1, network/testnet/amoy — served via
Cloudflare), selecting the live *Proxy entries the legacy SDK read;
1-hour stale-while-revalidate cache so long-running services pick up
redeployments without restart. A flat pre-resolved NetworkAddresses
object is accepted for the config.addresses override and custom
mirrors, and overrides get the same shape validation as fetched
indexes (full 20-byte-address checks).
- MintableERC1155Predicate and GasSwapper flow through the parser
(optional per network); predicate lookups are promise-cached per
token with eviction on failure.
Errors — typed, layered, and 0.x-compatible
- POSBridgeError extends VError (@polygonlabs/verror ^1.1.0):
findCauseByName / info / fullStack, closed 31-code discriminator
union, exhaustiveness-checked switches.
- Every condition that existed in 0.x ALSO carries the exact legacy
ERROR_TYPE string (typos preserved verbatim) on the same `type` field
the old SDK used — consumer switches written against 0.x keep
matching; LegacyErrorType is exported.
- Honest semantics: ALLOWED_ON_ROOT/ALLOWED_ON_CHILD for chain guards,
TRANSACTION_NOT_FOUND for transient not-mined states,
HTTP_REQUEST_FAILED for index/proof-API fetch failures (network-layer
fetch TypeErrors wrapped with cause), ROOT_HASH_RPC_FAILED reserved
for bor_getRootHash.
- httpGet redacts RPC tokens at MINT time (URL + body snippets), has a
30s timeout, and surfaces non-JSON 2xx bodies (CDN challenge pages)
with content-type + snippet instead of a bare SyntaxError.
- sanitiseError deep-walks message, stack, own props (info objects,
string arrays, nested request objects) and the cause chain — the
logger-agnostic layer over verror/logger's fingerprint-gated
auto-redaction.
Encapsulation policy — inspectable by design
- No hard-private (#) fields anywhere: adapters expose the exact
provider/signer (ethers) and publicClient/walletClient (viem) the
consumer passed in; token wrappers expose tokenAddress/isParent.
Internals are TypeScript-private — hidden from the typed API (and
still nominal in the d.ts) but plain properties at runtime, so
instances are debuggable, identity-comparable, and survive
Proxy-based reactivity (Vue reactive()/Pinia) that throws on
#-field libraries. An SDK has no security boundary against its own
consumer; the typed API is the supported surface.
Fast exits / bridge helpers
- Optional proofGenerationApiUrl (no default; opt-in, matching 0.x
setProofApi semantics), with correct per-network segments. If the
API is configured but fails, fast exits FALL BACK to local
construction (0.x parity) — an outage degrades fast exits to slow
ones instead of breaking withdrawals.
- Flat on POSClient: buildExitPayload(s)(OnIndex), isCheckpointed,
isDeposited, isWithdrawn(OnIndex), getBlockProof,
getPredicateAddress, getAddresses, getProofApi — restoring the
non-token surface downstream services rely on. Escape hatch:
pos.getAddresses() + the ABIs at @polygonlabs/pos-sdk/abi replace the
0.x `.method(...)` accessor.
- Reorg-safe checkpoint reads default to the 'safe' block tag; RPC
block parsing tolerates geth 1.14+ (no totalDifficulty); multi-output
contract reads decode positionally so viem's array decode and ethers'
Result both work.
ABIs — codegenned from @polygonlabs/meta, drift-gated
- src/abi/* is generated from @polygonlabs/meta (build-time devDep) by
scripts/generate-abis.ts; tsup inlines bytes AND declarations so the
published package has no runtime meta dependency. CI's
codegen-drift-check regenerates and fails on any diff.
Packaging — ESM-only, publish-safe
- version 0.0.0 (the 1.0.0 release is cut by changesets); prepublishOnly
builds; publishConfig.exports strips the internal @polygonlabs/source
condition; CHANGELOG.md ships; ethers peer floor ^5.6.0 (the adapter
requires BigNumber.toBigInt).
- ESM-only by necessity (runtime deps are ESM-only; the previous .cjs
artifacts could neither be reached nor loaded); require(esm)/dynamic
import paths documented. tsup code-splitting keeps one POSBridgeError
class identity across every entry, so cross-entry instanceof works.
- scripts/fix-dts-specifiers.mjs rewrites the ethers-v5 devDep alias to
'ethers' in shipped declarations and hard-fails the build if any dist
artifact references a build-time-only package.
Tests — the suite exercises the real world
- 119 unit/integration tests. Live-CDN test resolves BOTH networks
against static.polygon.technology on every run; parser fixtures pin
the nested-index extraction to @polygonlabs/meta's generated
snapshots.
- Exit-payload ground truth: fixtures record REAL mainnet burns (one
per token standard) with payloads from the production
proof-generation API, independently verified byte-identical to local
construction at recording time; the test rebuilds them creds-free
over public RPCs on every run. scripts/record-exit-fixture.mjs
automates re-recording (discover burn → API ground truth → verify
local === API → write).
- pack-smoke (scripts/pack-smoke.mjs) installs the packed tarball into
clean npm consumers (viem+ethers6, ethers5), imports every subpath,
checks cross-entry instanceof + legacy error types, and typechecks
probes with skipLibCheck:false.
- Examples are consumer-shaped (link:ed SDK, own install), runnable as
documented.
MIGRATION.md documents every breaking change with before/after blocks:
adapter factories, bigint, method renames, error codes + legacy type
mapping, per-tx options (gasPrice/type replacement), dropped config
and methods with replacements, address resolution, fast exits, ESM.
Includes the major changeset for the rename + redesign.
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
July 16, 2026 23:32
6123526 to
ceb6ed5
Compare
| constructor(config: ProofApiClientConfig) { | ||
| // Strip a single trailing slash so route composition never produces a | ||
| // double slash (`https://host//api/...`). | ||
| this.base = config.baseUrl.replace(/\/+$/, ''); |
…right Private workspace package that bundles the SDK's built dist through Vite and loads it in real Chromium via Playwright, asserting every public symbol is reachable and runs without console errors. The Vite build is itself the cross-environment gate: it uses NO Node polyfills and NO builtin stubs, so it fails outright if the SDK's dependency graph ever pulls in `events`, `buffer`, or another Node builtin — the failure mode the Node-based Vitest suites cannot see. (An earlier iteration papered over @ethereumjs v5-line transitives with throw-on-call stubs; the @ethereumjs/mpt v10 migration made the claim true and the stubs were deleted so the gate is real.) Exercises POSClient.init via the viemAdapter subpath factory, prepareApprove, POSBridgeError / VError, sanitiseError, noopLogger, the address-fetcher override, and the ethereum-cryptography keccak path. The script is `test:browser` (not `test`) so the root recursive `pnpm -r run test` never invokes Playwright in a job without browsers or a built bundle; the dedicated browser-smoke CI job builds the SDK, builds this bundle, installs Chromium, and runs it on every PR. The local skip guard probes by actually launching the browser, so a machine without the binary degrades to a clean skip.
MaximusHaximus
force-pushed
the
pos-sdk-1.0-rewrite
branch
from
July 16, 2026 23:47
ceb6ed5 to
9b8cc31
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
@maticnetwork/maticjs3.9.x accumulated a decade of abstraction debt with concrete failure modes: the plugin model mutated module globals (utils.Web3Client = X), making multi-tenant use unsafe; the lazyITransactionWriteResultconflated "submitted" vs "confirmed" (a caller assuminggetTransactionHash()was idempotent caused a production double-broadcast); theBaseToken → POSToken → ERC20chain forced non-token contracts to extend a token base; ABIs loaded from a single CDN at runtime (a global SPOF);BaseBigNumberpredated nativebigint; and 27: any+ 50ascasts left consumers with no compile-time safety.Scope: PoS only
This rewrite covers the PoS bridge. The zkEVM bridge is intentionally not ported — the zkEVM chain is winding down, so consumers using
ZkEvmClientstay on@maticnetwork/maticjs(the two install side by side during the window) rather than migrating twice. Details inMIGRATION.md.What we got
Construction — static, tree-shakeable adapter factories. Consumers wrap their existing client with a per-library factory imported from a subpath and pass it to
POSClient.init:The main entry imports no web3 library; you ship only the one you use. No plugin, no global state, no
kinddiscriminator.Cross-environment — proven, not claimed. Zero
Buffer, zeronode:*, zero dynamicimport(), zero Node-only transitives: the @ethereumjs v5-line is replaced by@ethereumjs/mptv10 (trie ops verified byte-identical), which removes theevents/buffer/readable-streamchains that would have forced browser polyfills. The Vite+Playwrighttest-appbundles the built dist with no stubs and no polyfills — the build itself fails if a Node builtin re-enters the graph — and runs the result in real Chromium on every PR.Type safety. 0
: any, 0as any. The remainingas unknown ascasts (all at the viem/ethers boundary insrc/adapters/) are now fenced by a path-scoped ESLint rule that bans the double-assertion everywhere else.as constABIs give viem-typed inference at every internal call site.Correctness / capability (driven by a parity audit against the old SDK).
proofGenerationApiUrl(opt-in, no default — matches 0.xsetProofApi) wires an internal client to the realproof-generation-apiroutes, fixing a 0.x mainnet-only network hardcode. The earlier rewrite accepted the URL but silently no-op'd it — a blocker the audit caught.pos.buildExitPayloads(←buildMultiplePayloadsForExit),pos.isDeposited(state-sync deposit confirmation), exposed flat alongsidebuildExitPayload,isCheckpointed,isWithdrawn,getBlockProof,getPredicateAddress— the non-token surfaceproof-generation-apiand similar consumers rely on.'safe'block tag (rootChainDefaultBlockto tune), restoring a guarantee the rewrite had dropped (it read atlatest).init(was always throwing).API ergonomics.
TxResult = { hash, confirmed() }— observe-only, idempotent.prepareXxxsibling on every write returns unsigned{ to, data, value? }for smart wallets (Safe / Sequence / AA bundlers), batching, and offline signing.parent/childnamespaces replace the invertibleisParent: boolean..method(...)):pos.getAddresses()surfaces the resolved bridge addresses (via the SWR cache) and theas constABIs export at@polygonlabs/pos-sdk/abi— pair them with your own client to call any unwrapped contract method, fully typed by your library.bigintthroughout; method renames (startWithdraw,completeWithdraw[Fast],soliditySha3).ABIs sourced from
@polygonlabs/meta(single source of truth). The contract ABIs are codegenned from@polygonlabs/meta— the published source of truth for Polygon ABIs — into localas constmodules (scripts/generate-abis.ts→src/abi/).@polygonlabs/metais a build-time (dev) dependency only: tsup inlines the ABI bytes and types intodist/, so the published package has no runtime dependency on it and consumers install nothing extra. The lockfile pins the exact meta version built against, and acodegen-drift-checkCI gate (via the sharedapps-codegen-drift-checkworkflow, with the generated modules markedlinguist-generated) fails if the committed ABIs ever diverge from their source — eliminating hand-vendored-ABI drift while keeping the runtime self-contained.Errors.
POSBridgeError extends VError(@polygonlabs/verror^1.1.0 —findCauseByName/info/fullStack, zero deps). Closed 31-code discriminator union with exhaustiveness-checked switches — and 0.x compatibility done honestly: every condition that existed in the legacy SDK also carries the exact oldERROR_TYPEstring (typos preserved verbatim) on the sametypefield the old SDK used, so migrated consumer switches keep matching. New honest semantics:ALLOWED_ON_ROOT/ALLOWED_ON_CHILDchain guards,TRANSACTION_NOT_FOUNDfor transient not-mined states,HTTP_REQUEST_FAILEDfor index/proof-API fetches (network-layer errors wrapped withcause). RPC tokens are redacted at error-mint time, and the exportedsanitiseErrordeep-walks info objects/arrays/cause chains for everything else.Packaging. webpack 4 → tsup, ESM-only (the SDK's runtime deps are ESM-only, so CJS artifacts could never load;
require(esm)/await import()paths documented). Code-splitting keeps onePOSBridgeErrorclass identity across all five entries so cross-entryinstanceofworks.prepublishOnlybuilds;publishConfig.exportsstrips the internal source condition; a post-build step rewrites theethers-v5devDep alias in shipped declarations and hard-fails if any dist artifact references a build-time-only package. Version sits at0.0.0— changesets cuts 1.0.0; the first publish is manual per our bootstrap process.abstracts/,implementation/,enums/deleted; source ~3,900 lines (from 5,674).Adversarial go-live review + full remediation. A 167-agent review (12 lenses, every finding adversarially verified) produced 8 blockers, 25 majors, 40 minors — all resolved on this branch. Highlights: the address parser was reading an index shape that never existed on the wire (now parses the real nested CDN files 3.9.x reads, at the correct per-network paths, verified against the Cloudflare-served endpoints); viem's positional tuple decode broke checkpoint lookups (now shape-tolerant);
totalDifficultyremoved by geth 1.14+ crashed block parsing (now optional);confirmed()cached rejections forever (now evicts); fast exits lost the 0.x local fallback (restored). The full review + fix history is in this branch's PR discussion.Tests exercise the real world. 119 unit/integration tests, and the gates the review found missing now run on every PR: a live-CDN test resolving both networks against
static.polygon.technology; an exit-payload ground-truth suite that locally rebuilds REAL mainnet burns (one per token standard) creds-free over public RPCs and byte-compares against payloads recorded from the production proof-generation API (re-recordable viascripts/record-exit-fixture.mjs); a packed-tarball smoke test on all three Node versions (clean npm consumers, every subpath imported,skipLibCheck:falsetype probes); and the browser smoke in real Chromium.Commits
chore(workspace)— tooling, CI gates (matrix + pack-smoke + browser-smoke + drift-check), Dependabot, theas unknown aslint ban.refactor(pos-sdk)!— the SDK + examples + README + MIGRATION + the major changeset.test(test-app)— the browser smoke test.Test plan
POS_SDK_TEST_PARENT_RPC,POS_SDK_TEST_CHILD_RPC,POS_SDK_TEST_PRIVATE_KEYso PR CI runs the integration tier (skips cleanly without them)changeset-release/masterPR appears after merge@polygonlabs/pos-sdkrelease, recover withpnpm exec changeset publishon the merge commitproof-generation-apitopos.buildExitPayload/buildExitPayloads/isCheckpointed/getBlockProof; auditportalandstaking-uifor legacypos.client.parent.Xcalls perMIGRATION.mdtests/fixtures/networks.ts@maticnetwork/maticjson npm (PoS users →@polygonlabs/pos-sdk; zkEVM users stay until chain shutdown)