npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@polygonlabs/maticjs

v4.0.0-pre.1

Published

TypeScript SDK for the Polygon PoS bridge and staking — supports viem, ethers v5, and ethers v6

Downloads

272

Readme

@polygonlabs/maticjs

npm version License: MIT

TypeScript SDK for the Polygon PoS bridge and PoS staking. Deposits, withdrawals, and exit proofs against RootChainManager — across ERC-20, ERC-721, ERC-1155 and native ETH — plus validator and delegator staking from @polygonlabs/maticjs/staking, driven by whichever EVM client library you already have: viem, ethers v5, or ethers v6.

@polygonlabs/maticjs 4.0 is the next major release of maticjs, moved from the @maticnetwork npm scope — versions up to 3.x remain published as @maticnetwork/maticjs. If you are migrating, read MIGRATION.md.

Install

pnpm add @polygonlabs/maticjs viem      # or
pnpm add @polygonlabs/maticjs ethers    # v5 or v6

viem and ethers are optional peer dependencies. Install only the one you use; the SDK keeps every cross-library import type-only at the module-load boundary so the absent peer never crashes.

ESM-only

The package ships ES modules only — there is no CommonJS build, because the SDK's own dependencies are ESM-only, so a .cjs artifact could never actually load. On Node ≥ 20 use import; from a CommonJS project use await import('@polygonlabs/maticjs') (or native require() of ES modules, supported unflagged on Node ≥ 20.19; Node 22.0–22.11 needs --experimental-require-module). TypeScript CommonJS consumers need "module": "nodenext" (or "node20", TypeScript ≥ 5.8); under node16, TypeScript reports TS1479 for any ESM-only package.

Inspectable by design

SDK objects deliberately avoid hard-private (#) fields: adapters expose the exact provider/signer (ethers) or publicClient/walletClient (viem) you passed in, and token wrappers expose tokenAddress/isParent — so you can verify wiring, compare identity against your own references, and debug in a REPL. Internals are TypeScript-private (hidden from the typed API but plain properties at runtime), which also means SDK instances survive Proxy-based reactivity systems (Vue reactive(), Pinia) without the Cannot read private member errors that #-field libraries like ethers v6 hit there. Anything not in the typed API remains unsupported surface — it may change in any release.

Quickstart (viem)

import { createPublicClient, createWalletClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { POSClient } from '@polygonlabs/maticjs';
import { viemAdapter } from '@polygonlabs/maticjs/viem';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const parentPublic = createPublicClient({ chain: sepolia, transport: http(process.env.PARENT_RPC) });
const parentWallet = createWalletClient({ account, chain: sepolia, transport: http(process.env.PARENT_RPC) });

const childPublic  = createPublicClient({ transport: http(process.env.CHILD_RPC)  });
const childWallet  = createWalletClient({ account, transport: http(process.env.CHILD_RPC)  });

const pos = await POSClient.init({
  network: 'amoy',
  parent: viemAdapter({ public: parentPublic, wallet: parentWallet }),
  child:  viemAdapter({ public: childPublic,  wallet: childWallet  })
});

// Read: balance of the bridged ERC-20 on the parent chain.
const erc20 = pos.parent.erc20(process.env.PARENT_TOKEN as `0x${string}`);
const balance = await erc20.getBalance(account.address);

// Write: approve the bridge predicate, then wait for inclusion.
const approve = await erc20.approve(1_000_000n);
console.log('approve hash:', approve.hash);
const receipt = await approve.confirmed();
console.log('approve mined in block:', receipt.blockNumber);

TxResult: separating "submitted" from "mined"

Every write returns

interface TxResult {
  hash: `0x${string}`;
  confirmed(): Promise<Receipt>;
}

hash is populated the moment the parent-chain RPC accepts the broadcast. confirmed() waits for the receipt and is memoised — subsequent calls reuse the same waiter rather than starting a second poll. The 3.x lazy result.getTransactionHash() / result.getReceipt() pattern is gone; consumers no longer have to remember which awaitable they are holding.

If the transaction mines but reverts, confirmed() rejects with a POSBridgeError coded TRANSACTION_REVERTED, with the receipt on err.info.receipt. If it is cancelled or replaced by a different transaction at the same nonce, confirmed() rejects with TRANSACTION_REPLACED. A fee-bumped re-send of the same transaction counts as the original. So a resolved confirmed() means the transaction, or a fee-bumped re-send of it, succeeded. See MIGRATION.md.

Smart wallets and unsigned transactions

Every public write has a sibling prepareXxx that returns the encoded transaction without broadcasting:

const tx = await pos.parent.erc20(token).prepareApprove(1_000n);
//   tx.to    — the contract to call
//   tx.data  — encoded calldata
//   tx.value — wei to attach (omitted when zero)

await safeClient.proposeTransaction({ to: tx.to, data: tx.data, value: tx.value ?? 0n });

Use this for Safe / Sequence / account abstraction bundlers, batched multicall flows, pre-flight inspection, or any path where the SDK should encode the bridge call but a different signer should send it. The prepared path is pure — no chain-id lookup, no gas estimation, no fee-cap guard.

Direct bridge helpers

Several methods are exposed flat on POSClient for consumers building exit payloads outside the standard token flows (sync block events, custom bridge events):

await pos.isCheckpointed(burnTxHash);                    // boolean
await pos.buildExitPayload(burnTxHash, eventSig, false); // exit calldata
await pos.getBlockProof(blockNumber, { start, end });    // Merkle proof
await pos.getPredicateAddress(token);                    // bridge predicate
await pos.isWithdrawn(burnTxHash, eventSig);             // already exited?

The token classes (pos.parent.erc20(...).completeWithdraw(...)) wrap these for the 95% case; reach for the flat methods only when you need them.

Calling contract methods the SDK doesn't wrap

When you need a bridge contract method that has no wrapper at all, drive the contract with the client you already passed in. The SDK hands you the two things it owns that you'd otherwise have to reproduce: the resolved address via pos.getAddresses() (served from the same stale-while-revalidate cache the bridge flows use) and the vendored as const ABI from the @polygonlabs/maticjs/abi subpath:

import { RootChainManagerABI } from '@polygonlabs/maticjs/abi';

const { RootChainManager } = await pos.getAddresses();
const value = await parentPublic.readContract({
  address: RootChainManager,
  abi: RootChainManagerABI,
  functionName: 'someMethodTheSdkDoesNotWrap',
  args: [/* ... */]
});

Your client infers argument and return types straight from the as const ABI — there's no SDK-specific call surface to learn, and the addresses still track index redeployments within the TTL window. This is the 4.0 replacement for the 3.x contract.method(name, ...args) accessor; see MIGRATION.md.

Dynamic address resolution

POSClient.init validates configuration by fetching the active address index for network from the Polygon CDN, caches it for one hour, and serves cached values stale-while-revalidate. Long-running services pick up Polygon contract redeployments without a restart: the next read after the TTL window kicks off a background refresh and keeps serving the cached value until it lands.

For air-gapped or staging deployments, override either the source URL (addressIndexUrl) or supply pre-resolved addresses directly (addresses: NetworkAddresses); both are documented on POSClientConfig in src/pos-client.ts.

Other clients

The same flow with ethers v5 or v6 — import the matching adapter factory from its subpath; everything else is identical. You only pull in the web3 library you actually use:

// ethers v5
import { ethersV5Adapter } from '@polygonlabs/maticjs/ethers-v5';
import { providers, Wallet } from 'ethers';
const provider = new providers.StaticJsonRpcProvider(process.env.PARENT_RPC);
const signer   = new Wallet(process.env.PRIVATE_KEY!, provider);
const pos = await POSClient.init({
  network: 'amoy',
  parent: ethersV5Adapter({ provider, signer }),
  child:  ethersV5Adapter({ provider: childProvider, signer: childSigner })
});

// ethers v6
import { ethersV6Adapter } from '@polygonlabs/maticjs/ethers-v6';
import { JsonRpcProvider, Wallet, Network } from 'ethers';
const provider = new JsonRpcProvider(process.env.PARENT_RPC, Network.from(11155111), { staticNetwork: true });
const signer   = new Wallet(process.env.PRIVATE_KEY!, provider);
const pos = await POSClient.init({
  network: 'amoy',
  parent: ethersV6Adapter({ provider, signer }),
  child:  ethersV6Adapter({ provider: childProvider, signer: childSigner })
});

Full runnable scripts (with env-var guards and an approve + read flow) live in the workspace examples/ directory.

Staking

Validator and delegator staking ships in the same package behind its own subpath, so bridge-only consumers carry no staking code. Staking lives on the parent chain, so StakingClient.init takes a single parent adapter built the same way as above:

import { StakingClient } from '@polygonlabs/maticjs/staking';
import { viemAdapter } from '@polygonlabs/maticjs/viem';

const staking = await StakingClient.init({
  network: 'mainnet',
  parent: viemAdapter({ public: publicClient, wallet: walletClient })
});

// Delegate POL to a validator: approve the StakeManager, then buy shares.
const validatorId = await staking.stakeManager.getValidatorId({ user: validatorOwner });
const share = staking.validatorShare(
  await staking.stakeManager.getValidatorContract({ validatorId })
);
const amount = 10n * 10n ** 18n;

await (await staking.pol.approveStakeManager({ amount })).confirmed();
const result = await share.delegate({ amount, minSharesToMint: 0n });
console.log(result.hash);
await result.confirmed();

Every staking write has an unsigned sibling for smart wallets and batchers — await share.prepareDelegate({ amount, minSharesToMint: 0n }) returns { to, data } — and reads return bigint or typed structs:

const info = await staking.stakeManager.getValidator({ validatorId });
const { stake } = await share.getTotalStake({ user: delegator });
const rewards = await share.getLiquidRewards({ user: delegator });

To pin addresses instead of fetching the address index, pass addresses: { StakeManager, MaticToken, PolToken } to StakingClient.init. Staking needs only those three, not the bridge addresses POSClient requires.

Coming from @maticnetwork/maticjs-staking? See the migration guide for the method mapping and the permit flow.

Errors

Every failure the SDK detects itself is a POSBridgeError with a stable discriminator code; errors from your RPC, wallet or client library are rethrown unchanged. Switch on code rather than parsing message strings:

import { POSBridgeError } from '@polygonlabs/maticjs';

try {
  await erc20.completeWithdraw(burnTxHash);
} catch (err) {
  if (err instanceof POSBridgeError) {
    switch (err.code) {
      case 'BURN_TX_NOT_CHECKPOINTED':
        // user-actionable: tell them to wait for the next checkpoint
        break;
      case 'PROOF_API_NOT_SET':
        // configuration: surface the missing `proofGenerationApiUrl` setting
        break;
      // ... TypeScript exhaustiveness-checks every code
    }
  }
  throw err;
}

The full code list is the POSBridgeErrorCode union exported from the SDK; see MIGRATION.md for a one-liner per code. Errors whose condition existed in the legacy SDK also carry the old 3.x string on err.type, so switches written against @maticnetwork/maticjs keep matching.

Redacting RPC tokens before logging

Many RPC endpoints carry auth tokens in their URL query string, and upstream fetch errors interpolate that URL into the error message. Before handing any SDK (or web3-library) error to your logger, run it through sanitiseError — it deep-walks the message, stack, structured info, and the whole cause chain, replacing token=<value> with token=*** without mutating the original:

import { sanitiseError } from '@polygonlabs/maticjs';

catch (err) {
  logger.error({ err: sanitiseError(err) }, 'bridge call failed');
}

The SDK's own errors never embed an unredacted token in the first place, and @polygonlabs/verror/@polygonlabs/logger users get additional automatic redaction of recognised ethers/viem fetch-error shapes at serialisation time — sanitiseError is the logging-stack- agnostic layer that covers everything else.

Documentation

License

MIT.

Third-party code bundled into the package, and its licence, is listed in THIRD_PARTY_NOTICES.md.