refactor: rewrite & modernization - @polygonlabs/pos-sdk@1.0.0 #476
Draft
MaximusHaximus wants to merge 5 commits into
Draft
refactor: rewrite & modernization - @polygonlabs/pos-sdk@1.0.0 #476MaximusHaximus wants to merge 5 commits into
MaximusHaximus wants to merge 5 commits into
Conversation
| # is not blown by checkpoint waits. | ||
| POS_SDK_TEST_E2E_ENABLED: 'true' | ||
| steps: | ||
| - uses: 0xPolygon/pipelines/.github/actions/ci@main |
e410c24 to
a4870f3
Compare
a4870f3 to
d0d0b5f
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(/\/+$/, ''); |
d0d0b5f to
df65838
Compare
df65838 to
4162952
Compare
Workspace-level scaffolding for the @polygonlabs/pos-sdk 1.0 rewrite;
no package source. Wires up the build/lint/CI gates and the planning
record so the rewrite lands cleanly in the per-package commits on top.
- 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 (same reason); pnpm-lock
regenerated with the SDK's deps (@polygonlabs/verror, p-limit,
ethereum-cryptography, @ethereumjs/*) and the test-app's vite/playwright.
- 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). `: any` / `as any`
remain banned globally by the preset.
- CI: ci-trigger.yml forwards POS_SDK_TEST_{PARENT_RPC,CHILD_RPC,
PRIVATE_KEY} via job.env and builds @polygonlabs/pos-sdk on Node
20/22/24; ci-nightly.yml (new) runs the gated e2e cycle daily.
- plans/pos-sdk-1.0-rewrite.md + PLAN.md + PLAN_BACKUP.md: the
agent-executable plan and strategic notes, retained as the design
record.
- Removes the stale empty .changeset/old-dolls-prove.md.
…-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 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 `kind` discriminator, no dynamic
imports anywhere — fully static.
Cross-environment
- Zero `Buffer`, zero `node:*`, zero dynamic import: runs unchanged in
Node >=20 and modern browsers. Byte code (RLP / merkle / proofs) uses
Uint8Array + ethereum-cryptography; byte-pinning tests prove parity.
Core surface
- POSClient.init({ network, parent, child }) — only construction path.
- TxResult = { hash, confirmed() } (observe-only; idempotent).
- bigint everywhere; `as const` ABIs with no runtime ABI dependency
(the 0.x runtime-CDN ABI fetch is gone); parent/child namespaces;
method renames (startWithdraw, completeWithdraw[Fast], soliditySha3).
- prepareXxx sibling on every write returns unsigned { to, data, value? }
for smart wallets / batching / offline signing.
- Dynamic contract addresses via stale-while-revalidate (1h TTL);
config.addresses override for air-gapped.
- Reorg-safe checkpoint reads default to the 'safe' block tag
(rootChainDefaultBlock to tune).
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 (the npm face of
0xPolygon/static) — by scripts/generate-abis.ts, emitting local
`as const` modules under src/abi/. meta is a build-time (dev)
dependency only: tsup inlines the ABI bytes and types into dist/, so
the published package has no runtime dependency on meta and consumers
install nothing extra. The lockfile pins the exact meta version built
against, and the shared codegen-drift-check workflow (codegen-drift-check
script + .gitattributes linguist-generated marker) fails CI if the
committed ABIs ever diverge from meta — so they cannot silently drift.
Errors
- POSBridgeError extends VError (Joyent verror's TS-first,
browser-friendly port): findCauseByName / info / fullStack, code
discriminator, name pinned for log aggregation. Closed 27-code union.
Fast exits / bridge helpers
- Optional proofGenerationApiUrl (no default; opt-in, matching 0.x
setProofApi semantics) wires an internal client to the real
proof-generation-api routes (/api/v1/<matic|amoy>/...), fixing the
0.x mainnet-only network hardcode.
- Flat on POSClient: buildExitPayload, buildExitPayloads,
buildExitPayloadOnIndex, isCheckpointed, isDeposited, isWithdrawn[
OnIndex], getBlockProof, getPredicateAddress — restoring the
non-token capabilities (buildMultiplePayloadsForExit, isDeposited)
that consumers like proof-generation-api depend on.
- Mintable ERC-1155 predicate wired through init.
Tooling / tests / docs
- webpack 4 → tsup (ESM + CJS + DTS; index + three adapter subpath
entries; viem/ethers externalized). Source ~3,900 lines (from 5,674);
abstracts/, implementation/, enums/ deleted.
- Unit + skipIf-gated live-chain integration + gated e2e tests.
- README + MIGRATION (full removed-API replacement tables, factory
adapter API, proof config, error codes) + examples for all three
libraries.
Includes the major changeset for the rename + redesign.
- Escape hatch (replaces 0.x `.method(...)`): `pos.getAddresses()` surfaces
the resolved bridge addresses (through the same stale-while-revalidate
cache), and the `as const` ABIs (codegenned from @polygonlabs/meta at
build time) are exported at the `@polygonlabs/pos-sdk/abi` subpath.
Consumers pair the two with their own client to call contract methods
the SDK does not wrap — fully typed by their library, no SDK-specific
call surface.
…right Private workspace package that bundles the SDK through Vite and loads it in a real Chromium browser via Playwright, asserting every public symbol is reachable and runs without tripping a Node-only global. This is the cross-environment safety net the Node-based Vitest suites can't provide: a Buffer/process reference inside a transitive dep only breaks when bundled for the browser. Exercises POSClient.init via the viemAdapter subpath factory, prepareApprove (static viem encodeFunctionData), POSBridgeError / VError, sanitiseError, noopLogger, the address-fetcher override, and the ethereum-cryptography keccak path. Asserts no console errors fired. The skip guard probes by actually launching the browser in beforeAll and skips the suite on failure, so `pnpm -r run test` degrades to a clean skip when the Playwright browser binary is absent (a static executablePath() check is insufficient — headless runs use the separate chrome-headless-shell binary). CI installs the browser via `playwright install --with-deps chromium` and runs it for real.
4162952 to
bf978ef
Compare
httpGet checked res.ok but still parsed the body with a bare res.json(), so a CDN/WAF serving a 200 HTML challenge page produced a context-free SyntaxError with no URL and no body. Read the body as text first and throw a POSBridgeError carrying the URL, status, content-type, and a 200-char body snippet for both failure shapes (non-2xx now includes the snippet too). Completes the 1.0 counterpart of the 0.x metadata-fetch error-visibility fix (2dc5cc7); the 0.x retry counterpart (9dc06a2) is intentionally not ported — it existed for node-fetch's stale keep-alive 'Premature close', and 1.0 uses native fetch only.
Dependabot (the org's dependency bot; Renovate is not installed) keeps the workspace current — most importantly the @polygonlabs/meta devDependency, whose releases arrive as individual ungrouped PRs where the codegen-drift-check gate regenerates the ABIs and the suite runs against them. Routine npm bumps are grouped weekly; GitHub Actions likewise. meta 1.1.1 lowers its published engines.node floor to >=20 (the package is pure data; 24 was only ever the build-toolchain requirement) — verified the generated ABIs are byte-identical to 1.1.0.
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. Zero
Buffer, zeronode:*, zero dynamicimport()— runs unchanged in Node ≥20 and modern browsers. The byte code (RLP / merkle / proofs) usesUint8Array+ethereum-cryptography; byte-pinning tests prove parity. A Vite+Playwrighttest-apppackage loads the bundle in a real browser to keep it honest.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(Joyent verror's TS-first, browser-friendly port —findCauseByName/info/fullStack, zero deps). Closed 27-code discriminator union; codes match the 0.xErrorHelperkeys so existing dashboards keep matching. The vagueBRIDGE_ADAPTER_NOT_FOUNDand the now-unreachableUNSUPPORTED_PROVIDERwere removed;CONTRACT_NOT_AVAILABLE_ON_NETWORKadded for honest network-capability conditions.Cleanup. webpack 4 → tsup (ESM + CJS + DTS; index + three adapter subpath entries, libraries externalized).
abstracts/,implementation/,enums/deleted. Source ~3,900 lines (from 5,674).Commits
chore(workspace)— tooling, CI (incl. test secrets + nightly e2e), theas unknown aslint ban, plan docs.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/integration/exit-payload.test.ts; verify test-token addresses intests/fixtures/networks.ts@maticnetwork/maticjson npm (PoS users →@polygonlabs/pos-sdk; zkEVM users stay until chain shutdown)