@perspectivefi/addresses 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,213 @@
1
- # Temporary Holding Version
1
+ # @perspectivefi/addresses
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 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`.
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ Token addresses stay in `token-list`. Pools, PTs and vaults are not listed one by one (see [Pools inherit from factories](#pools-inherit-from-factories)).
10
+
11
+ ## The four statuses
12
+
13
+ | Status | What it means | Allowed on QA | Allowed in prod |
14
+ | ------------- | ------------------------------------------------- | ------------- | --------------- |
15
+ | `unsafe` | Deployed, code not audited | yes | no |
16
+ | `role-unsafe` | Audited, but the deployer still holds admin roles | yes | no |
17
+ | `safe` | Audited, and admin roles handed to the DAO | yes | yes |
18
+ | `blocked` | Explicitly banned, e.g. a test pool | yes | no |
19
+
20
+ 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.
21
+
22
+ ## Data format
23
+
24
+ One file per chain in `registry/`: `registry/<chainId>.json` (e.g. `1.json`, `8453.json`) or `registry/stellar.json`.
25
+
26
+ ```json
27
+ {
28
+ "chain": 1,
29
+ "name": "Ethereum",
30
+ "governance": ["0x..."],
31
+ "entries": [
32
+ {
33
+ "key": "core.router",
34
+ "address": "0x...",
35
+ "kind": "contract",
36
+ "status": "unsafe",
37
+ "tags": ["core"],
38
+ "source": { "repo": "perspectivefi/...", "commit": "<40-hex sha>" },
39
+ "audit": "https://...",
40
+ "deployer": "0x...",
41
+ "inherit": true,
42
+ "notes": "..."
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ | Field | Notes |
49
+ | ------------ | ------------------------------------------------------------------------------------------------ |
50
+ | `governance` | DAO Safe / timelock addresses allowed to hold admin roles on this chain. |
51
+ | `key` | `<domain>.<name>`, lowercase, dots and dashes, unique per chain. This is what code asks for. |
52
+ | `address` | EVM: EIP-55 checksummed. Stellar: `C...` 56-character contract id. |
53
+ | `kind` | `contract`, `factory`, `beacon`, `instance` or `external` (third party). |
54
+ | `source` | Repo and commit the deployed code came from. Optional for `unsafe` and `external`. |
55
+ | `audit` | Link to the audit report. Optional for `unsafe` and `external`. |
56
+ | `deployer` | Optional deployer EOA. The nightly and PR role check make sure it holds no admin role on `safe`. |
57
+ | `inherit` | Only on `factory` / `beacon`: instances created by it inherit its status. |
58
+
59
+ ## How to add an address
60
+
61
+ 1. Add an entry to `registry/<chain>.json` with status `unsafe`. Use the checksummed address. If the chain has no file yet, create it (name = chain id) and fill `governance`.
62
+ 2. Run `npm run validate`, and `npm run onchain` if you want to see the chain check locally.
63
+ 3. Open a PR. CI runs the checks below; you need 2 approvals.
64
+
65
+ Run `npm run generate` after editing a registry file and commit `src/generated/registry.ts` as well. CI fails if it is stale.
66
+
67
+ ## How to promote a status
68
+
69
+ Each step is a PR that changes one `status` line (plus the fields it needs).
70
+
71
+ | Change | What you add | What CI checks |
72
+ | ------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `unsafe` -> `role-unsafe` | `source.repo` + 40-hex `source.commit`, `audit` URL | The fields exist and are well-formed. The contract has bytecode on-chain. |
74
+ | `role-unsafe` -> `safe` | Hand admin roles to the DAO, then change status | Everything above, plus the **on-chain role check**: every owner / admin found is in `governance`, and the `deployer` (if set) holds no admin role. |
75
+ | anything -> `blocked` | A reason in `notes` | The contract has bytecode. |
76
+
77
+ 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). RPC failures on a `safe` entry fail the run.
78
+
79
+ Every other check, on every PR:
80
+
81
+ - JSON shape and types, `chain` matches the file name, keys are well-formed and unique per chain.
82
+ - EVM addresses are EIP-55 checksummed, Stellar ids are valid, `governance` addresses are valid.
83
+ - `inherit` only on factories / beacons.
84
+ - The same address under two keys gives a warning, not a failure.
85
+ - Every address has bytecode on-chain (Stellar is not checked yet).
86
+
87
+ **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.
88
+
89
+ ## Pools inherit from factories
90
+
91
+ 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.
92
+
93
+ If someone creates a test pool through a prod factory, add that single pool to the registry as `blocked` (`kind: "instance"`). A `blocked` entry always wins over the factory's status.
94
+
95
+ ## Using it in an app
96
+
97
+ ```ts
98
+ // QA, previews, API server: every address, whatever its status
99
+ import { address, requireAddress, isProdSafe } from '@perspectivefi/addresses'
100
+
101
+ // Production front-ends: only `safe` addresses exist in this bundle
102
+ import { address, requireAddress } from '@perspectivefi/addresses/prod'
103
+
104
+ address(1, 'otc.engine') // string | undefined (undefined in prod unless safe: hide the feature)
105
+ requireAddress(1, 'otc.engine') // throws "no address for key ... on chain 1" instead
106
+ ```
107
+
108
+ Pick the entry point at build time (alias `@perspectivefi/addresses` to `.../prod` in production builds). Both have the same API:
109
+
110
+ | Function | Returns |
111
+ | ----------------------------- | ------------------------------------------------------ |
112
+ | `address(chain, key)` | the address or `undefined` |
113
+ | `requireAddress(chain, key)` | the address or throws an error naming chain and key |
114
+ | `entry(chain, key)` | the full `Entry` or `undefined` |
115
+ | `findByAddress(chain, addr)` | the `Entry` (EVM compare is case-insensitive) |
116
+ | `statusOf(chain, addr)` | `Status` or `undefined` |
117
+ | `isProdSafe(chain, addr)` | `true` only for `safe` |
118
+ | `entries(chain?)`, `chains()` | all entries (optionally one chain), the chains present |
119
+
120
+ 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.
121
+
122
+ The package has zero runtime dependencies. The `/prod` bundle contains no non-safe address at all (a test checks `dist/prod.*`).
123
+
124
+ ### Release check (CLI)
125
+
126
+ `spectra-addresses check` scans a finished build for contract addresses and fails the release if one is not `safe` in the registry, or not in the registry at all.
127
+
128
+ ```jsonc
129
+ // package.json of the app
130
+ "build": "next build && spectra-addresses check .next --allow token-list-addresses.json"
131
+ ```
132
+
133
+ ```
134
+ spectra-addresses check <paths...> [--mode warn|enforce] [--allow <json file>]... [--registry <url|file>] [--offline]
135
+ ```
136
+
137
+ - **Mode**: `--mode`, else env `ADDRESS_GATE`, else `warn`. In `warn` it only prints. In `enforce` it exits 1 on violations, but only when `VERCEL_ENV` is unset or `production`; in a Vercel preview build `enforce` is downgraded to `warn`. The effective mode is printed.
138
+ - **Latest registry**: the check does not use the registry version your lockfile pins. It fetches `https://cdn.jsdelivr.net/npm/@perspectivefi/addresses@latest/dist/registry.json` (10 second timeout), so an address marked `blocked` today stops today's release even when the app still pins an older package. Override the source with `--registry <url-or-file>` or env `ADDRESS_REGISTRY_URL` (the flag wins). The output says which dataset was used.
139
+ - **Fallback**: if the fetch or the JSON shape check fails, the check falls back to the dataset bundled in the installed package and prints a `WARNING` that names the bundled version. This also applies in `enforce` mode: a jsDelivr outage does not fail your build, but the run is only as fresh as the installed version. `--offline` skips the fetch and uses the bundled dataset on purpose.
140
+ - Scans files recursively for EVM addresses and Stellar contract ids (skips `*.map`, `cache` and `node_modules` dirs, and binary files over 5 MB).
141
+ - Each unique address is checked against every chain: `safe` anywhere is ok; listed but not safe is a violation (shows key, chain, status); not listed is a violation (`unknown`).
142
+ - 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.
143
+ - Exit codes: `0` ok / warn, `1` violations in effective `enforce`, `2` bad usage.
144
+
145
+ ### ESLint preset (no hardcoded addresses)
146
+
147
+ `@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.
148
+
149
+ ```js
150
+ // eslint.config.mjs
151
+ import addresses from '@perspectivefi/addresses/eslint'
152
+
153
+ export default [
154
+ // ...your other configs
155
+ addresses,
156
+ {
157
+ // tests and fixtures may contain raw addresses
158
+ files: ['**/*.test.{ts,tsx}', '**/__tests__/**', '**/fixtures/**', '**/mocks/**'],
159
+ rules: { 'no-restricted-syntax': 'off' },
160
+ },
161
+ ]
162
+ ```
163
+
164
+ ESLint keeps one `no-restricted-syntax` setting per file, so the preset replaces yours. If you already use that rule, merge the selectors instead:
165
+
166
+ ```js
167
+ import addresses from '@perspectivefi/addresses/eslint'
168
+
169
+ rules: {
170
+ 'no-restricted-syntax': ['error', ...addresses.restrictedSyntax, /* your own */],
171
+ }
172
+ ```
173
+
174
+ (`restrictedSyntax`, `hardcodedAddressRules` and `literalKeyRules` are available as properties of the default export.)
175
+
176
+ ## Slack announcements
177
+
178
+ After each successful publish, the workflow posts the registry changes to Slack (#nt-deployment-addresses-announcements): new addresses grouped by chain and domain (with the chain's DAO Safe as admin), address changes, status changes and removals. Changes to tags, notes, source or audit only are not announced, and nothing is posted when nothing relevant changed. If more than 50 addresses are added at once, only a per-chain count is posted. Messages stay under about 3500 characters and are split between chains when needed.
179
+
180
+ 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.
181
+
182
+ Preview locally, without posting anything:
183
+
184
+ ```
185
+ npx tsx scripts/announce.ts --from HEAD~1 --to HEAD --dry-run
186
+ ```
187
+
188
+ ## Development
189
+
190
+ ```
191
+ npm ci
192
+ npm run generate # registry/*.json -> src/generated/registry.ts (commit the result)
193
+ npm run validate # static checks on registry/*.json
194
+ npm run typecheck
195
+ npm test # generates, builds, then runs vitest
196
+ npm run build # dist/index, dist/prod, dist/cli.js, dist/registry.json
197
+ npm run onchain [-- --report out.json]
198
+ ```
199
+
200
+ `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.
201
+
202
+ 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.
203
+
204
+ ## Branch protection to apply on `main`
205
+
206
+ Apply this in GitHub (Settings -> Rules / Branches) on `perspectivefi/spectra-addresses`:
207
+
208
+ - Require a pull request before merging.
209
+ - Required approvals: **2**.
210
+ - **Dismiss stale approvals** when new commits are pushed.
211
+ - Require status checks to pass: **`validate`** (the job in `.github/workflows/validate.yml`); require branches to be up to date.
212
+ - **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.
213
+ - Block force pushes and branch deletion.