npm package: @polygonlabs/spol-contracts-sdk
Description: Typed [viem](https://viem.sh) contract clients for the sPOL protocol, covering L1 (Ethereum mainnet / Sepolia) and L2 (Polygon / Amoy).
Latest version: 2.0.1

# spol-contracts-sdk

Typed [viem](https://viem.sh) contract clients for the sPOL protocol, covering L1 (Ethereum mainnet / Sepolia) and L2 (Polygon / Amoy).

The sPOL protocol spans ~10 smart contracts across two chains. Without a central SDK, every service
would copy the same ABIs, hardcode the same addresses, and redefine the same event signatures —
diverging silently as the protocol evolves. This package provides a single source of truth: ABIs
defined once, addresses defaulting to production values (overridable per-service via env vars), and
event signatures co-located with their ABI entries.

## Development

```bash
bun run build      # compile to dist/
bun run typecheck  # type-check without emitting
```

## Entry points

| Entry point                    | Contents                                                  |
| ------------------------------ | --------------------------------------------------------- |
| `spol-contracts-sdk`           | `createL1Contracts`, `createL2Contracts` — primary API    |
| `spol-contracts-sdk/events`    | Per-contract event objects `{ signature, eventABI }`      |
| `spol-contracts-sdk/factories` | Per-contract factory functions (custom address use cases) |
| `spol-contracts-sdk/abis`      | Raw ABI exports (non-viem use cases)                      |

## Usage

### Primary API — `createL1Contracts` / `createL2Contracts`

Pass any viem `PublicClient` or `WalletClient`. The chain ID is read from the client to select
the right contract addresses automatically. All contracts and event objects for that chain are
returned together in one bundle.

```ts
import { createL1Contracts, createL2Contracts } from 'spol-contracts-sdk';
import { createPublicClient, http } from 'viem';
import { mainnet, polygon } from 'viem/chains';

const l1Contracts = createL1Contracts(
  createPublicClient({ chain: mainnet, transport: http(l1RpcUrl) })
);
const l2Contracts = createL2Contracts(
  createPublicClient({ chain: polygon, transport: http(l2RpcUrl) })
);
```

**Why this over direct factory/ABI imports:**

- All contracts for a chain in one object — no scattered imports or address lookups.
- Chain ID drives address selection — no network-conditional branching, no address env vars to manage.
- Type safety propagates from your client: `PublicClient` → read-only methods; `WalletClient` → write methods too.
- Throws at construction on an unsupported chain — fails fast rather than silently at call time.

### Calling contract functions

```ts
const totalSupply = await l1Contracts.spol.read.totalSupply();
const nonce = await l1Contracts.spolController.read.globalWithdrawNonce();
const hash = await l1Contracts.spolMessenger.write.updateL2ExchangeRate(); // WalletClient only
```

### Getting a contract address

Addresses live on the contract object — access them directly rather than importing a separate
address module.

```ts
const controllerAddress = l1Contracts.spolController.address;
const childAddress = l2Contracts.sPOLChild.address;
```

The correct address for the connected chain is always present.

### Events (from contracts)

Events are bundled alongside contract instances — prefer this when you already have a client.

```ts
// Topic hash for filtering raw logs
l2Contracts.sPOLChildEvents.ExchangeRateUpdated.signature;
l1Contracts.withdrawManagerEvents.ExitStarted.signature;

// Typed ABI entry for event consumer configs
l2Contracts.sPOLChildEvents.sPOLMinted.eventABI;
l1Contracts.sPOLMessengerEvents.MigrationProcessed.eventABI;
```

---

## Sub-paths

Use only when the primary API doesn't fit your context.

### `spol-contracts-sdk/events`

Use when you need event objects but have no viem client — typically when building event consumer
configs at service startup before a client exists:

```ts
import {
  sPOLChildEvents,
  sPOLMessengerEvents,
  withdrawManagerEvents
} from 'spol-contracts-sdk/events';

const config: IEventConsumerConfig = {
  contractAddress: '0x...',
  events: [
    sPOLChildEvents.sPOLMinted.eventABI,
    sPOLChildEvents.sPOLBurned.eventABI
  ] as unknown as IEventConsumerConfig['events']
};
```

Each event entry shape: `{ signature: '0x...', eventABI: { type, name, inputs, anonymous } } as const`.

The `as unknown as` cast is required because viem's `as const` produces readonly types narrower
than the mutable array type some consumer frameworks expect.

### `spol-contracts-sdk/abis`

Only when passing a raw ABI to non-viem code (e.g. `ethers.Contract`):

```ts
import { sPOLControllerAbi } from 'spol-contracts-sdk/abis';
const contract = new ethers.Contract(address, sPOLControllerAbi, provider);
```

### `spol-contracts-sdk/factories`

Only when you need a viem contract at a custom address not in the SDK config:

```ts
import { getSPolChildContract } from 'spol-contracts-sdk/factories';
const contract = getSPolChildContract({ address: customAddress, client });
```

---

## Supported chains

| Layer | Chain            | Chain ID |
| ----- | ---------------- | -------- |
| L1    | Ethereum mainnet | 1        |
| L1    | Sepolia          | 11155111 |
| L2    | Polygon          | 137      |
| L2    | Amoy             | 80002    |

Passing a client on an unsupported chain throws immediately with a clear error listing the
supported chain IDs.

---

## Contract addresses

Default addresses are hardcoded in `src/config.ts` and can be overridden via environment
variables — useful for local development or fork testing. Invalid values are silently ignored
and the hardcoded default is used instead.

| Chain            | Contract         | Env var                      |
| ---------------- | ---------------- | ---------------------------- |
| Ethereum mainnet | sPOL             | `MAINNET_SPOL`               |
| Ethereum mainnet | sPOLController   | `MAINNET_SPOL_CONTROLLER`    |
| Ethereum mainnet | sPOLMessenger    | `MAINNET_SPOL_MESSENGER`     |
| Ethereum mainnet | polBridger       | `MAINNET_POL_BRIDGER`        |
| Ethereum mainnet | rootChainManager | `MAINNET_ROOT_CHAIN_MANAGER` |
| Ethereum mainnet | stakeManager     | `MAINNET_STAKE_MANAGER`      |
| Ethereum mainnet | validatorShare   | `MAINNET_VALIDATOR_SHARE`    |
| Ethereum mainnet | withdrawManager  | `MAINNET_WITHDRAW_MANAGER`   |
| Sepolia          | sPOL             | `SEPOLIA_SPOL`               |
| Sepolia          | sPOLController   | `SEPOLIA_SPOL_CONTROLLER`    |
| Sepolia          | sPOLMessenger    | `SEPOLIA_SPOL_MESSENGER`     |
| Sepolia          | polBridger       | `SEPOLIA_POL_BRIDGER`        |
| Sepolia          | rootChainManager | `SEPOLIA_ROOT_CHAIN_MANAGER` |
| Sepolia          | stakeManager     | `SEPOLIA_STAKE_MANAGER`      |
| Sepolia          | validatorShare   | `SEPOLIA_VALIDATOR_SHARE`    |
| Sepolia          | withdrawManager  | `SEPOLIA_WITHDRAW_MANAGER`   |
| Polygon          | sPOLChild        | `POLYGON_SPOL_CHILD`         |
| Polygon          | polBridger       | `POLYGON_POL_BRIDGER`        |
| Amoy             | sPOLChild        | `AMOY_SPOL_CHILD`            |
| Amoy             | polBridger       | `AMOY_POL_BRIDGER`           |