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 |
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(/\/+$/, ''); |
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; vendored `as const` ABIs (CDN dep for ABIs 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).
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 vendored `as const` ABIs 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.
d0d0b5f to
df65838
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. 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 the vendoredas 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).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)