Skip to content

Commit 707b0a0

Browse files
ryanioclaudedevin-ai-integration[bot]
committed
Release v2.9.1
Origin-SHA: 5f657f94ac77da0ecbcc5efdf264d0ba3cca707d ### Patch Changes - c5aada3: `events list` no longer accepts a chain filter. `GET /api/v2/events` has no `chain` parameter and returns events from every chain whatever is sent, so the flag never filtered anything. `opensea events list --chain <chain>` and `opensea --chain <chain> events list` now exit with an error that points to `events by-account <address> --chain <chain>` and `events by-nft <chain> <contract> <token-id>`, which do filter by chain. The programmatic `EventsAPI.list` stops sending `chain`, and its `chain` option is deprecated. Correction to 2.9.0: its notes list `events list` among the commands whose `--chain` filter was restored. That filter never applied on this endpoint; only `collections list` and `events by-account` filter by chain. - f5676f0: `opensea tokens holders` now describes the distribution it returns: total holders, the share of eligible supply held by the top 250, and a health score and label. - Updated dependencies [6ffe0c9] - Updated dependencies [e7882a8] - Updated dependencies [a1e4356] - Updated dependencies [f5676f0] - @opensea/api-types@0.14.2 - @opensea/sdk@12.10.2 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 986c097 commit 707b0a0

11 files changed

Lines changed: 146 additions & 56 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pnpm run format
2424
1. **Never hand-roll API request/response types**. Import from `@opensea/api-types` via `src/types/api.ts`. Run `pnpm --filter @opensea/api-types run update-spec && pnpm --filter @opensea/api-types run build` before adding endpoints.
2525
2. **API path check**. `pnpm check-api-paths` (from the repo root) fails if source code references an `/api/v2/...` URL not in `packages/api-types/opensea-api.json`.
2626
3. **CLI commands are thin**. Parse args, call `OpenSeaClient`, format output with `formatOutput()` (or `outputGet()` from `src/output.ts` for plain GET commands, which also handles TOON). Keep business logic in the client.
27-
4. **One file per domain in `src/commands/`**. Register new commands in `src/commands/index.ts` and wire in `src/cli.ts`.
27+
4. **One file per domain in `src/commands/`**. Export new commands from `src/commands/index.ts` and register them with `program.addCommand()` in `src/program.ts` (`createProgram()`).
2828
5. **SDK classes in `src/sdk.ts`**. Add domain classes as `readonly` properties on `OpenSeaCLI` for programmatic consumers.
2929
6. **Auth session invariants**. Persisted tokens require access token, refresh token, wallet address, ISO expiration, scopes, and `oauth`/`siwe` method. Normalize only `0x`-prefixed EVM addresses to lowercase; preserve Solana base58 case.
3030
7. **Exit codes**. `0` success, `1` API error, `2` auth error, `3` rate limited.

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# @opensea/cli
22

3+
## 2.9.1
4+
5+
### Patch Changes
6+
7+
- c5aada3: `events list` no longer accepts a chain filter. `GET /api/v2/events` has no `chain` parameter and returns events from every chain whatever is sent, so the flag never filtered anything. `opensea events list --chain <chain>` and `opensea --chain <chain> events list` now exit with an error that points to `events by-account <address> --chain <chain>` and `events by-nft <chain> <contract> <token-id>`, which do filter by chain. The programmatic `EventsAPI.list` stops sending `chain`, and its `chain` option is deprecated.
8+
9+
Correction to 2.9.0: its notes list `events list` among the commands whose `--chain` filter was restored. That filter never applied on this endpoint; only `collections list` and `events by-account` filter by chain.
10+
11+
- f5676f0: `opensea tokens holders` now describes the distribution it returns: total holders, the share of eligible supply held by the top 250, and a health score and label.
12+
- Updated dependencies [6ffe0c9]
13+
- Updated dependencies [e7882a8]
14+
- Updated dependencies [a1e4356]
15+
- Updated dependencies [f5676f0]
16+
- @opensea/api-types@0.14.2
17+
- @opensea/sdk@12.10.2
18+
319
## 2.9.0
420

521
### Minor Changes

‎docs/cli-reference.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ Full command reference for all `opensea` CLI commands.
1515
```
1616

1717
A `--chain` you pass reaches every subcommand that takes `--chain` (such as
18-
`collections list`, `events list` or `drops deploy`), whether you put it before
19-
or after the subcommand. The `ethereum` default never does: a chain filter you
20-
leave out filters nothing, and a command that needs a chain fails without one.
18+
`collections list`, `events by-account` or `drops deploy`), whether you put it
19+
before or after the subcommand. The `ethereum` default never does: a chain
20+
filter you leave out filters nothing, and a command that needs a chain fails
21+
without one. `events list` has no chain filter and refuses a `--chain` you
22+
pass; use `events by-account --chain` or `events by-nft <chain>` instead.
2123

2224
## Authentication
2325

@@ -243,7 +245,7 @@ opensea transactions receipt --request <receipt-request.json>
243245
## Events
244246

245247
```bash
246-
opensea events list [--event-type <type>] [--after <timestamp>] [--before <timestamp>] [--chain <chain>] [--limit <n>] [--next <cursor>]
248+
opensea events list [--event-type <type>] [--after <timestamp>] [--before <timestamp>] [--limit <n>] [--next <cursor>]
247249
opensea events by-account <address> [--event-type <type>] [--chain <chain>] [--limit <n>] [--next <cursor>]
248250
opensea events by-collection <slug> [--event-type <type>] [--limit <n>] [--next <cursor>]
249251
opensea events by-nft <chain> <contract> <token-id> [--event-type <type>] [--limit <n>] [--next <cursor>]

‎package-lock.json‎

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "2.9.0",
3+
"version": "2.9.1",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -23,8 +23,8 @@
2323
"prepublishOnly": "npm run build"
2424
},
2525
"dependencies": {
26-
"@opensea/api-types": "^0.14.0",
27-
"@opensea/sdk": "^12.10.1",
26+
"@opensea/api-types": "^0.14.2",
27+
"@opensea/sdk": "^12.10.2",
2828
"@opensea/wallet-adapters": "^1.2.1",
2929
"commander": "^14.0.3",
3030
"zod": "^4.3.6"

‎src/commands/events.ts‎

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Command } from "commander"
1+
import { Command, Option } from "commander"
22
import type { OpenSeaClient } from "../client.js"
33
import type { OutputFormat } from "../output.js"
44
import { outputGet } from "../output.js"
@@ -17,32 +17,40 @@ export function eventsCommand(
1717
const cmd = new Command("events").description("Query marketplace events")
1818

1919
addPaginationOptions(
20-
addChainOption(
21-
cmd
22-
.command("list")
23-
.description("List events")
24-
.option(
25-
"--event-type <type>",
26-
"Event type (sale, transfer, mint, listing, offer, trait_offer, collection_offer)",
27-
)
28-
.option(
29-
"--after <timestamp>",
30-
"Filter events after this Unix timestamp",
31-
)
32-
.option(
33-
"--before <timestamp>",
34-
"Filter events before this Unix timestamp",
35-
),
36-
),
20+
cmd
21+
.command("list")
22+
.description("List events")
23+
.option(
24+
"--event-type <type>",
25+
"Event type (sale, transfer, mint, listing, offer, trait_offer, collection_offer)",
26+
)
27+
.option("--after <timestamp>", "Filter events after this Unix timestamp")
28+
.option(
29+
"--before <timestamp>",
30+
"Filter events before this Unix timestamp",
31+
)
32+
// GET /api/v2/events has no chain filter. The option stays declared,
33+
// hidden, so a --chain in either position reaches this action and is
34+
// refused rather than silently returning events from every chain.
35+
.addOption(new Option("--chain <chain>").hideHelp()),
3736
).action(
38-
async (options: {
39-
eventType?: string
40-
after?: string
41-
before?: string
42-
chain?: string
43-
limit: string
44-
next?: string
45-
}) => {
37+
async (
38+
options: {
39+
eventType?: string
40+
after?: string
41+
before?: string
42+
chain?: string
43+
limit: string
44+
next?: string
45+
},
46+
command: Command,
47+
) => {
48+
if (options.chain !== undefined) {
49+
command.error(
50+
"error: events list cannot filter by chain, because the events endpoint has no chain filter. Use 'opensea events by-account <address> --chain <chain>' or 'opensea events by-nft <chain> <contract> <token-id>' instead.",
51+
{ code: "opensea.unsupportedChainFilter" },
52+
)
53+
}
4654
const client = getClient()
4755
await outputGet(client, getFormat(), "/api/v2/events", {
4856
event_type: options.eventType,
@@ -52,7 +60,6 @@ export function eventsCommand(
5260
before: options.before
5361
? parseIntOption(options.before, "--before")
5462
: undefined,
55-
chain: options.chain,
5663
limit: parseIntOption(options.limit, "--limit"),
5764
next: options.next,
5865
})

‎src/commands/tokens.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ export function tokensCommand(
266266
cmd
267267
.command("holders")
268268
.description(
269-
"Get paginated holders for a token (with aggregate distribution health)",
269+
"Get paginated holders for a token, with its distribution: total holders, the share of eligible supply held by the top 250, and a health score and label",
270270
)
271271
.argument("<chain>", "Chain")
272272
.argument("<address>", "Token contract address"),

‎src/sdk.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,11 @@ class EventsAPI {
904904
before?: number
905905
limit?: number
906906
next?: string
907+
/**
908+
* @deprecated Ignored and not sent. `GET /api/v2/events` has no `chain`
909+
* parameter, so it never filtered. Use `byAccount` with `chain`, or
910+
* `byNFT`, for chain-scoped events.
911+
*/
907912
chain?: Chain
908913
}): Promise<{ asset_events: AssetEvent[]; next?: string }> {
909914
return this.client.get("/api/v2/events", {
@@ -912,7 +917,6 @@ class EventsAPI {
912917
before: options?.before,
913918
limit: options?.limit,
914919
next: options?.next,
915-
chain: options?.chain,
916920
})
917921
}
918922

‎test/cli-chain-option.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ describe("subcommand --chain through the full program", () => {
155155
describe("--chain filters", () => {
156156
it.each([
157157
[["collections", "list"], "/api/v2/collections"],
158-
[["events", "list"], "/api/v2/events"],
159158
[
160159
["events", "by-account", "0x1111111111111111111111111111111111111111"],
161160
"/api/v2/events/accounts/0x1111111111111111111111111111111111111111",
@@ -178,6 +177,38 @@ describe("subcommand --chain through the full program", () => {
178177
})
179178
})
180179

180+
// GET /api/v2/events has no chain parameter and returns every chain's
181+
// events whatever is sent, so a chain the user asks for is refused.
182+
describe("events list", () => {
183+
it.each([
184+
["after the subcommand", ["events", "list", "--chain", "base"]],
185+
["before the subcommand", ["--chain", "base", "events", "list"]],
186+
])("refuses --chain placed %s", async (_, argv) => {
187+
const stderr = vi
188+
.spyOn(process.stderr, "write")
189+
.mockImplementation(() => true)
190+
vi.spyOn(process, "exit").mockImplementation(() => {
191+
throw new Error("process.exit")
192+
})
193+
194+
await expect(run(...argv)).rejects.toThrow("process.exit")
195+
196+
expect(fetchSpy).not.toHaveBeenCalled()
197+
expect(stderr.mock.calls.flat().join("")).toContain(
198+
"events list cannot filter by chain",
199+
)
200+
})
201+
202+
it("runs without --chain and sends no chain", async () => {
203+
respondWith({ asset_events: [] })
204+
205+
await run("events", "list")
206+
207+
expect(requestedUrl().pathname).toBe("/api/v2/events")
208+
expect(requestedUrl().searchParams.has("chain")).toBe(false)
209+
})
210+
})
211+
181212
// Commander checks a mandatory option before any hook runs, so a
182213
// `requiredOption("--chain ...")` on a subcommand can never be satisfied:
183214
// the program has already taken the value. Required chains are checked in

‎test/commands/events.test.ts‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,43 @@ describe("eventsCommand", () => {
2727
ctx.mockClient.get.mockResolvedValue({ asset_events: [] })
2828

2929
const cmd = eventsCommand(ctx.getClient, ctx.getFormat)
30-
await cmd.parseAsync(
31-
["list", "--event-type", "sale", "--limit", "5", "--chain", "ethereum"],
32-
{ from: "user" },
33-
)
30+
await cmd.parseAsync(["list", "--event-type", "sale", "--limit", "5"], {
31+
from: "user",
32+
})
3433

3534
expect(ctx.mockClient.get).toHaveBeenCalledWith(
3635
"/api/v2/events",
3736
expect.objectContaining({
3837
event_type: "sale",
3938
limit: 5,
40-
chain: "ethereum",
4139
}),
4240
)
41+
expect(ctx.mockClient.get.mock.calls[0][1]).not.toHaveProperty("chain")
42+
})
43+
44+
it("list subcommand refuses --chain instead of sending an ignored filter", async () => {
45+
const cmd = eventsCommand(ctx.getClient, ctx.getFormat)
46+
cmd.exitOverride()
47+
cmd.commands.find(c => c.name() === "list")?.exitOverride()
48+
const stderr = vi
49+
.spyOn(process.stderr, "write")
50+
.mockImplementation(() => true)
51+
52+
await expect(
53+
cmd.parseAsync(["list", "--chain", "base"], { from: "user" }),
54+
).rejects.toMatchObject({ code: "opensea.unsupportedChainFilter" })
55+
56+
expect(ctx.mockClient.get).not.toHaveBeenCalled()
57+
const message = stderr.mock.calls.flat().join("")
58+
expect(message).toContain("events list cannot filter by chain")
59+
expect(message).toContain("events by-account <address> --chain <chain>")
60+
expect(message).toContain("events by-nft <chain>")
61+
})
62+
63+
it("list help does not advertise --chain", () => {
64+
const cmd = eventsCommand(ctx.getClient, ctx.getFormat)
65+
const list = cmd.commands.find(c => c.name() === "list")
66+
expect(list?.helpInformation()).not.toContain("--chain")
4367
})
4468

4569
it("list subcommand parses after/before timestamps", async () => {

0 commit comments

Comments
 (0)