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

@perspectivefi/addresses

v0.2.9

Published

Contract address registry for Spectra: one list of every address, each with a status. Prod only accepts `safe`.

Readme

@perspectivefi/addresses

The contract address registry for Spectra. One shared list of every contract address, where each address has a status. Production only accepts addresses marked safe.

It exists so that test contracts stop reaching prod. Addresses used to be copied by hand into every repo, and the copies drifted apart. Now there is one list, changes go through reviewed PRs, and CI checks them against the chain.

The registry decides whether something may go to prod, not whether it is listed. Existing curation (whitelists, API catalog filters) still decides what users see.

Token addresses stay in token-list. Pools, PTs and vaults are not listed one by one (see Pools inherit from factories).

What safe means and who may approve it: POLICY.md. What this package does and does not guarantee: Guarantees.

The four statuses

| Status | What it means | Allowed on QA | Allowed in prod | | ------------- | ------------------------------------------------- | ------------- | --------------- | | unsafe | Deployed, code not audited | yes | no | | role-unsafe | Audited, but the deployer still holds admin roles | yes | no | | safe | Audited, and admin roles handed to the DAO | yes | yes | | blocked | Explicitly banned, e.g. a test pool | yes | no |

A contract moves up as it matures: unsafe -> role-unsafe -> safe. Each step is a small PR. For third-party contracts (Kyber, Pendle, Multicall3...) safe means "approved integration"; we don't audit them.

Data format

One file per chain in registry/: registry/<chainId>.json (e.g. 1.json, 8453.json) or registry/stellar.json.

{
    "chain": 1,
    "name": "Ethereum",
    "governance": ["0x..."],
    "entries": [
        {
            "key": "core.router",
            "address": "0x...",
            "kind": "contract",
            "status": "unsafe",
            "tags": ["core"],
            "source": { "repo": "perspectivefi/...", "commit": "<40-hex sha>" },
            "audit": "https://...",
            "deployer": "0x...",
            "inherit": true,
            "approval": { "basis": "audit", "pr": 12, "note": "..." },
            "notes": "..."
        }
    ],
    "denylist": [
        {
            "address": "0x...",
            "reason": "test vault removed from the registry; must never be listed in prod"
        }
    ]
}

| Field | Notes | | ------------ | ------------------------------------------------------------------------------------------------ | | governance | DAO Safe / timelock addresses allowed to hold admin roles on this chain. | | key | <domain>.<name>, lowercase, dots and dashes, unique per chain. This is what code asks for. | | address | EVM: EIP-55 checksummed. Stellar: C... 56-character contract id. | | kind | contract, factory, beacon, instance or external (third party). | | source | Repo and commit the deployed code came from. Optional for unsafe and external. | | audit | Link to the audit report. Optional for unsafe and external. | | deployer | Optional deployer EOA. The nightly and PR role check make sure it holds no admin role on safe. | | inherit | Only on factory / beacon: instances created by it inherit its status. | | approval | { basis, pr, note? }. Required on every safe entry, see POLICY.md. | | denylist | Per chain file, optional: { address, reason } items. These addresses are never acceptable. |

approval.basis is one of audit, prod-baseline, external-integration, governance; approval.pr is the registry PR that approved the entry. approval is not part of the runtime data, only of dist/registry.json.

Denylist

A blocked entry must never simply be deleted: once it is gone the address is just "unknown" (or, behind a safe factory, looks inherited-safe). Move it to the chain's denylist instead. A denylisted address:

  • is blocked for statusOf(chain, address) even without an entry, and isDenied(chain, address) is true;
  • is shipped in both the full and the /prod datasets, so consumers can still refuse it;
  • fails the build check wherever it appears, on that chain;
  • cannot also be an entry (validation error); addresses must be valid, unique and have a non-empty reason.

CI enforces this: npm run validate -- --base origin/main fails when a blocked entry of the base branch has disappeared and is not in the denylist.

How to add an address

  1. Add an entry to registry/<chain>.json with status unsafe (never straight to safe: approval is a separate PR, see POLICY.md). Use the checksummed address. If the chain has no file yet, create it (name = chain id) and fill governance.
  2. Run npm run validate, and npm run onchain if you want to see the chain check locally.
  3. Open a PR. CI runs the checks below; you need 2 approvals.

Run npm run generate after editing a registry file and commit the files in src/generated/ as well. CI fails if it is stale.

How to promote a status

Each step is a PR that changes one status line (plus the fields it needs).

| Change | What you add | What CI checks | | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | unsafe -> role-unsafe | source.repo + 40-hex source.commit, audit URL | The fields exist and are well-formed. The contract has bytecode on-chain. | | role-unsafe -> safe | Hand admin roles to the DAO, then change status, add approval | Everything above, plus approval.pr equal to this PR's number, plus the on-chain role check: every owner / admin found is in governance, and the deployer (if set) holds no admin role. | | anything -> blocked | A reason in notes | The contract has bytecode. |

source and audit are required for role-unsafe, and for safe unless the entry's approval.basis is something other than audit (prod-baseline, external-integration, governance; the evidence is then in the PR, see POLICY.md).

The on-chain role check reads owner(), the EIP-1967 admin slot (and the owner of that admin contract if it is one), and authority() for OpenZeppelin AccessManager (hasRole(0, governance[i]) must be true for at least one governance address, and hasRole(0, deployer) must be false). An owner or proxy-admin owner that is itself an AccessManager, and an AccessManaged proxy admin (it has authority() instead of owner()), are judged the same way. RPC failures on a safe entry fail the run.

Every other check, on every PR:

  • JSON shape and types, chain matches the file name, keys are well-formed and unique per chain.
  • EVM addresses are EIP-55 checksummed, Stellar ids are valid, governance addresses are valid.
  • inherit only on factories / beacons.
  • approval is well-formed and present on every safe entry; in a PR, an entry that is newly safe must carry this PR's number in approval.pr.
  • denylist is valid (see above) and no blocked entry was deleted compared with the base branch.
  • The same address under two keys gives a warning, not a failure.
  • Every address has bytecode on-chain (Stellar is not checked yet).

Nightly: the on-chain run repeats daily. If a safe entry no longer passes (roles changed after approval), it opens an issue labelled registry-drift (or comments on the open one). Nothing is downgraded automatically; a human decides.

Pools inherit from factories

There are hundreds of pools, PTs and vaults and new ones appear all the time, so they are not listed. They inherit the status of the factory that created them: a pool made by a safe factory is safe. Mark a factory or beacon with "inherit": true to make that explicit.

If someone creates a test pool through a prod factory, add that single pool to the registry as blocked (kind: "instance"). A blocked entry, and a denylisted address, always win over the factory's status. Runtime data carries inherit: true on the factories / beacons that opted in; the package only exposes the data, consumers decide how to verify provenance.

Using it in an app

// QA, previews, API server: every address, whatever its status
import { address, requireAddress, isProdSafe } from '@perspectivefi/addresses'

// Production front-ends: only `safe` addresses exist in this bundle
import { address, requireAddress } from '@perspectivefi/addresses/prod'

address(1, 'otc.engine') // string | undefined  (undefined in prod unless safe: hide the feature)
requireAddress(1, 'otc.engine') // throws "no address for key ... on chain 1" instead

Pick the entry point at build time (alias @perspectivefi/addresses to .../prod in production builds). Both have the same API:

| Function | Returns | | ----------------------------- | -------------------------------------------------------------- | | address(chain, key) | the address or undefined | | requireAddress(chain, key) | the address or throws an error naming chain and key | | entry(chain, key) | the full Entry or undefined | | findByAddress(chain, addr) | the Entry (EVM compare is case-insensitive) | | statusOf(chain, addr) | Status, blocked for a denylisted address, else undefined | | isDenied(chain, addr) | true when the address is on that chain's denylist | | isProdSafe(chain, addr) | true only for safe | | entries(chain?), chains() | all entries (optionally one chain), the chains present |

Types: Entry, Status, Kind, ChainRef (chain ids + 'stellar'), AddressKey (every key in the registry). chain also accepts a plain number, e.g. a runtime chain id.

The package has zero runtime dependencies. The /prod bundle contains no non-safe entry at all (a test checks dist/prod.*); the only non-safe addresses in it are the denylisted ones, which are there so they can be refused.

Per-key exports (tree-shakeable)

For front-ends, import only the keys you need. Unused keys are dropped by the bundler, so only those addresses end up in the bundle:

import { coreRouter } from '@perspectivefi/addresses/prod/keys' // safe addresses only (production)
import { coreRouter } from '@perspectivefi/addresses/keys' // every status (QA, previews)

coreRouter[1] // string | undefined, a frozen { chain -> address } record

One export per registry key, named in camelCase: core.router -> coreRouter, external.zodiac.roles-mastercopy-v2.1.0 -> externalZodiacRolesMastercopyV2_1_0 (a segment that starts with a digit is joined with _). Generation fails if two keys would get the same name. In /prod/keys a key with no safe chain is an empty frozen object, so imports never break; check the chain you need and hide the feature when it is missing.

Each value is emitted as JSON.parse("{\"$spectraAddresses\":1,\"key\":\"core.router\",\"chains\":{...}}"). Minifiers never rewrite string literals, so the release check finds these blocks verbatim in the built bundle and checks every (chain, address) pair. The package declares sideEffects: false.

Release check (CLI)

spectra-addresses check scans a finished build and fails the release when it finds an address that is not approved for the chain it is used on.

// package.json of the app
"build": "next build && spectra-addresses check .next --allow token-list-addresses.json"
spectra-addresses check <paths...> [--mode off|enforce] [--allow <json file>]... [--registry <url|file>] [--offline]
  • Mode: --mode, else env ADDRESS_GATE. There are two modes. off prints the full report and always exits 0. enforce exits 1 on any violation. If VERCEL_ENV=production and the mode is unset or invalid, the check exits 1 ("ADDRESS_GATE must be set to off or enforce in production"), so a missing setting can never silently disable protection. Outside production an unset or invalid mode means off. enforce is not downgraded on preview builds.
  • Tagged blocks first: every $spectraAddresses block (from /keys, /prod/keys, the runtime datasets and the runtime denylist) is found, also when its JSON is escaped inside a JS string. For each (chain, address) in a block, the exact entry for that chain must be safe in the latest registry and not denylisted. The report names key and chain. A block that cannot be parsed, or carries anything besides the fields we emit, is a violation. The runtime denylist block is accepted only for addresses the registry itself denylists.
  • Then bare addresses: the blocks are cut out and the rest of the file is scanned for EVM addresses and Stellar contract ids. A bare address passes only if it is on an --allow list, or every registry entry with that address (on any chain) is safe and it is denylisted on no chain. Otherwise it is a violation, and the report says which chain made it fail (blocked, unsafe, denylisted, or not in registry). A denylisted address fails even if it is on an --allow list.
  • Latest registry: the check does not use the version your lockfile pins. jsDelivr and unpkg cache @latest URLs for hours, so it first asks npm for the exact latest version (https://registry.npmjs.org/@perspectivefi/addresses/latest, printed as latest published version: x.y.z) and then fetches dist/registry.json of that version from jsDelivr (https://cdn.jsdelivr.net/npm/@perspectivefi/addresses@<version>/dist/registry.json), then from unpkg (https://unpkg.com/@perspectivefi/addresses@<version>/dist/registry.json), 10 second timeout each. If the npm lookup fails it prints a warning and uses the @latest CDN URLs instead. --registry <url-or-file> or env ADDRESS_REGISTRY_URL replaces all of this (the flag wins; no npm lookup). The output says which dataset was used.
  • If no source loads: in off the check prints a WARNING and uses the dataset bundled in the installed package. In enforce it exits 1 ("cannot verify against the latest registry"). --offline skips the fetch and uses the bundled dataset on purpose; in enforce it prints a loud warning that the run is only as fresh as the installed version.
  • Scans files recursively (skips *.map, cache and node_modules dirs, and binary files over 5 MB).
  • Always allowed: the zero address and 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. --allow takes JSON files with an array of address strings (or objects with an address field). Use it for token-list addresses. It applies to bare addresses only.
  • Exit codes: 0 ok / off, 1 violations in enforce, an unset mode in production, or an unverifiable registry in enforce, 2 bad usage.

dist/registry.json is { "entries": [...], "denylist": [...] } (every field, every status). Versions before 0.2.0 published a bare array; their CLI cannot read the new shape and falls back to its bundled data, so upgrade the CLI together with the first release that uses this one.

ESLint preset (no hardcoded addresses)

@perspectivefi/addresses/eslint is a flat-config object (plain JS, no ESLint dependency). It errors on any string or template literal that is an EVM address (zero address allowed) or a Stellar contract id, and on address() / requireAddress() calls whose 2nd argument is not a string literal.

// eslint.config.mjs
import addresses from '@perspectivefi/addresses/eslint'

export default [
    // ...your other configs
    addresses,
    {
        // tests and fixtures may contain raw addresses
        files: ['**/*.test.{ts,tsx}', '**/__tests__/**', '**/fixtures/**', '**/mocks/**'],
        rules: { 'no-restricted-syntax': 'off' },
    },
]

ESLint keeps one no-restricted-syntax setting per file, so the preset replaces yours. If you already use that rule, merge the selectors instead:

import addresses from '@perspectivefi/addresses/eslint'

rules: {
    'no-restricted-syntax': ['error', ...addresses.restrictedSyntax, /* your own */],
}

(restrictedSyntax, hardcodedAddressRules and literalKeyRules are available as properties of the default export.)

Guarantees

What this package guarantees, and what it does not.

Covered

  • Lookups are per chain. address, statusOf, findByAddress, isDenied and the per-key records never mix chains. An address approved on one chain says nothing about the same address on another.
  • Production build scan. spectra-addresses check runs against the latest published registry (not the pinned one) and fails the release when a built bundle contains an address that is not safe on its chain: tagged per-key blocks are checked per chain, and any other address-looking literal (the "bare-address rule") must be safe on every chain it is listed on, denylisted nowhere, or be on an --allow list. Unknown addresses fail. With the mode set to enforce this blocks the release; in production the mode must be set explicitly.
  • Removed means refused, not forgotten. Deleting a blocked entry requires moving it to the denylist, and the denylist is in the prod dataset too.
  • Every safe entry has an approval record (basis and registry PR), and in CI a newly safe entry must be approved by the PR that introduces it.
  • Nightly on-chain check that safe entries still have their roles with the DAO. It opens an issue; nothing is downgraded automatically.

Not covered here

  • The API runtime gate lives in the consumers. This package supplies data (statusOf, isDenied, inherit, entries); the API decides what to serve, and how to verify that a pool or vault really comes from an approved factory.
  • No RPC in consumers. The package never calls a chain. On-chain checks (bytecode, roles) run only in this repo's CI and nightly job.
  • App addresses change only with a release. A frontend bundle contains the addresses it was built with. A status change in the registry reaches an app through its next release, where the build scan checks it against the latest registry; it does not change a bundle that is already deployed. Likewise the API only sees registry changes when it is rebuilt with a newer package.
  • Addresses assembled at runtime (concatenated strings, fetched from elsewhere) cannot be found by the scan. Tokens come from token-list.
  • The runtime datasets are chain-aware too. /prod (and the full dataset) is emitted as a tagged block of (chain, key, address) entries, so a prod build that uses address(1, 'core.access-manager') is judged on chain 1 only, even if the same address is a different, unapproved contract on another chain.

Slack announcements

After each successful publish, the workflow posts the registry changes to Slack (#nt-deployment-addresses-announcements), most important first:

  1. ✅ Approved for prod: entries whose status changed to safe (or that were added as safe), with the entry's approval reason (string, or approval.basis) when it has one.
  2. ⛔ Blocked: status changes to blocked, additions that are blocked (these are not repeated under the additions), and denylist additions and removals (denylist + 0x… / denylist - 0x…, with the reason). An entry that moved to the denylist is reported here, not as a removal.
  3. New addresses grouped by chain and domain.
  4. Address changes, other status changes, and removals.

Changes to tags, notes, source or audit only are not announced, and nothing is posted when nothing relevant changed. Large changes are summarised as one line per chain and domain (ETHEREUM · Legacy 29) plus a link to the full list: more than 50 additions, or more than 20 removals. Messages stay under about 3500 characters and are split between blocks when needed.

Setup: a Slack admin creates an incoming webhook for the channel, and it is stored as the repository secret SLACK_ADDRESSES_WEBHOOK_URL. If the secret is missing or Slack is down, the step prints a ::warning:: and the publish still succeeds.

Preview locally, without posting anything:

npx tsx scripts/announce.ts --from HEAD~1 --to HEAD --dry-run

Development

npm ci
npm run generate    # registry/*.json -> src/generated/{registry,keys,keys-prod}.ts (commit the result)
npm run validate    # static checks on registry/*.json
npm run validate -- --base origin/main   # also compare with the base branch (CI does this on PRs)
npm run typecheck
npm test            # generates, builds, then runs vitest
npm run build       # dist/index, dist/prod, dist/keys, dist/prod-keys, dist/cli.js, dist/registry.json
npm run onchain [-- --report out.json]

npm run onchain uses RPC_URL_<chainId> when set, otherwise viem's default public RPC for the chain. Stellar entries are skipped for now ("not supported yet"). ONCHAIN_CONCURRENCY (default 4) limits parallel RPC work.

Merging to main publishes a new version to npm (trusted publishing). The version is <major>.<minor> from package.json plus the workflow run number, and is never committed back (main is protected); bump major or minor in a PR.

Branch protection to apply on main

Apply this in GitHub (Settings -> Rules / Branches) on perspectivefi/spectra-addresses:

  • Require a pull request before merging.
  • Required approvals: 2.
  • Dismiss stale approvals when new commits are pushed.
  • Require status checks to pass: validate (the job in .github/workflows/validate.yml); require branches to be up to date.
  • No bypass for anyone, including admins: enable "Do not allow bypassing the above settings" / leave the bypass list empty, and turn on "Include administrators" if using classic branch protection.
  • Block force pushes and branch deletion.