Skip to content

Commit b4244ac

Browse files
ryanioclaudedevin-ai-integration[bot]douglasborthwick-cryptodumanoglu1
committed
Release v2.7.0
Origin-SHA: d8b784e51583e5cc3fd02c6a9268bd3395644983 ### Minor Changes - 931433c: Reject numeric option values that are not entirely numeric. `--limit 10foo`, `--limit 1.9`, `--limit 1e2`, `--limit 0x10`, and `--slippage 0.01%` used to be truncated to their numeric prefix and now fail with an "Invalid value" error. Integer options also reject values above `Number.MAX_SAFE_INTEGER`, and float options reject values that overflow to `Infinity`. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: douglasborthwick-crypto <douglasborthwick-crypto@users.noreply.github.com> Co-authored-by: dumanoglu1 <dumanoglu1@users.noreply.github.com> Co-authored-by: iborazzi <iborazzi@users.noreply.github.com>
1 parent d6d5f7e commit b4244ac

7 files changed

Lines changed: 267 additions & 127 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pnpm run format
2323

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`.
26-
3. **CLI commands are thin**. Parse args, call `OpenSeaClient`, format output with `formatOutput()`. Keep business logic in the client.
26+
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.
2727
4. **One file per domain in `src/commands/`**. Register new commands in `src/commands/index.ts` and wire in `src/cli.ts`.
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.

‎CHANGELOG.md‎

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

3+
## 2.7.0
4+
5+
### Minor Changes
6+
7+
- 931433c: Reject numeric option values that are not entirely numeric. `--limit 10foo`, `--limit 1.9`, `--limit 1e2`, `--limit 0x10`, and `--slippage 0.01%` used to be truncated to their numeric prefix and now fail with an "Invalid value" error. Integer options also reject values above `Number.MAX_SAFE_INTEGER`, and float options reject values that overflow to `Infinity`.
8+
39
## 2.6.1
410

511
### Patch Changes

‎package-lock.json‎

Lines changed: 119 additions & 119 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.6.1",
3+
"version": "2.7.0",
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.12.0",
27-
"@opensea/sdk": "^12.9.0",
26+
"@opensea/api-types": "^0.12.1",
27+
"@opensea/sdk": "^12.9.1",
2828
"@opensea/wallet-adapters": "^1.2.1",
2929
"commander": "^14.0.3",
3030
"zod": "^4.3.6"

‎src/parse.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ export function readJsonBodyOption<T = unknown>(
2929
}
3030

3131
export function parseIntOption(value: string, name: string): number {
32-
const parsed = Number.parseInt(value, 10)
33-
if (Number.isNaN(parsed)) {
32+
const parsed = Number(value)
33+
if (!/^[+-]?\d+$/.test(value) || !Number.isSafeInteger(parsed)) {
3434
throw new Error(`Invalid value for ${name}: "${value}" is not an integer`)
3535
}
3636
return parsed
3737
}
3838

3939
export function parseFloatOption(value: string, name: string): number {
40-
const parsed = Number.parseFloat(value)
41-
if (Number.isNaN(parsed)) {
40+
const parsed = Number(value)
41+
if (
42+
!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value) ||
43+
!Number.isFinite(parsed)
44+
) {
4245
throw new Error(`Invalid value for ${name}: "${value}" is not a number`)
4346
}
4447
return parsed

‎test/commands/transactions.test.ts‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
2+
import { tmpdir } from "node:os"
3+
import { join } from "node:path"
4+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
5+
import { transactionsCommand } from "../../src/commands/transactions.js"
6+
import { type CommandTestContext, createCommandTestContext } from "../mocks.js"
7+
8+
function writeTempFile(contents: string): { dir: string; file: string } {
9+
const dir = mkdtempSync(join(tmpdir(), "opensea-cli-transactions-"))
10+
const file = join(dir, "request.json")
11+
writeFileSync(file, contents)
12+
return { dir, file }
13+
}
14+
15+
describe("transactionsCommand", () => {
16+
let ctx: CommandTestContext
17+
18+
beforeEach(() => {
19+
ctx = createCommandTestContext()
20+
})
21+
22+
afterEach(() => {
23+
vi.restoreAllMocks()
24+
})
25+
26+
it("creates the receipt subcommand", () => {
27+
const cmd = transactionsCommand(ctx.getClient, ctx.getFormat)
28+
29+
expect(cmd.name()).toBe("transactions")
30+
expect(cmd.commands.map(command => command.name())).toContain("receipt")
31+
})
32+
33+
it("posts the parsed request body to the receipt endpoint", async () => {
34+
ctx.mockClient.post.mockResolvedValue({ status: "confirmed" })
35+
const body = { swap_quote: { id: "quote-1" } }
36+
const { dir, file } = writeTempFile(JSON.stringify(body))
37+
38+
const cmd = transactionsCommand(ctx.getClient, ctx.getFormat)
39+
try {
40+
await cmd.parseAsync(["receipt", "--request", file], { from: "user" })
41+
} finally {
42+
rmSync(dir, { recursive: true, force: true })
43+
}
44+
45+
expect(ctx.mockClient.post).toHaveBeenCalledWith(
46+
"/api/v2/transactions/receipt",
47+
body,
48+
)
49+
expect(ctx.consoleSpy).toHaveBeenCalledWith(
50+
JSON.stringify({ status: "confirmed" }, null, 2),
51+
)
52+
})
53+
54+
it("names the option and path when the request file is missing", async () => {
55+
const dir = mkdtempSync(join(tmpdir(), "opensea-cli-transactions-"))
56+
const missing = join(dir, "missing.json")
57+
58+
const cmd = transactionsCommand(ctx.getClient, ctx.getFormat)
59+
try {
60+
await expect(
61+
cmd.parseAsync(["receipt", "--request", missing], { from: "user" }),
62+
).rejects.toThrow(`--request: could not read or parse '${missing}': `)
63+
} finally {
64+
rmSync(dir, { recursive: true, force: true })
65+
}
66+
expect(ctx.mockClient.post).not.toHaveBeenCalled()
67+
})
68+
69+
it("names the option and path when the request file is invalid JSON", async () => {
70+
const { dir, file } = writeTempFile("{ not json }")
71+
72+
const cmd = transactionsCommand(ctx.getClient, ctx.getFormat)
73+
try {
74+
await expect(
75+
cmd.parseAsync(["receipt", "--request", file], { from: "user" }),
76+
).rejects.toThrow(`--request: could not read or parse '${file}': `)
77+
} finally {
78+
rmSync(dir, { recursive: true, force: true })
79+
}
80+
expect(ctx.mockClient.post).not.toHaveBeenCalled()
81+
})
82+
83+
it("requires --request", async () => {
84+
const cmd = transactionsCommand(ctx.getClient, ctx.getFormat)
85+
const receipt = cmd.commands.find(command => command.name() === "receipt")
86+
expect(receipt).toBeDefined()
87+
receipt?.exitOverride()
88+
89+
await expect(cmd.parseAsync(["receipt"], { from: "user" })).rejects.toThrow(
90+
"error: required option '--request <file>' not specified",
91+
)
92+
expect(ctx.mockClient.post).not.toHaveBeenCalled()
93+
})
94+
})

‎test/parse.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,38 @@ describe("parseIntOption", () => {
2727
'Invalid value for --limit: "" is not an integer',
2828
)
2929
})
30+
31+
it.each([
32+
"10foo",
33+
"1.9",
34+
"1e2",
35+
"0x10",
36+
" 10",
37+
"10\n",
38+
])('throws when "%s" is not entirely an integer', value => {
39+
expect(() => parseIntOption(value, "--limit")).toThrow(
40+
`Invalid value for --limit: "${value}" is not an integer`,
41+
)
42+
})
43+
44+
it("accepts the largest safe integer and rejects the next one", () => {
45+
expect(parseIntOption("9007199254740991", "--before")).toBe(
46+
Number.MAX_SAFE_INTEGER,
47+
)
48+
expect(() => parseIntOption("9007199254740992", "--before")).toThrow(
49+
'Invalid value for --before: "9007199254740992" is not an integer',
50+
)
51+
})
3052
})
3153

3254
describe("parseFloatOption", () => {
3355
it("parses valid floats", () => {
3456
expect(parseFloatOption("0.5", "--slippage")).toBe(0.5)
3557
expect(parseFloatOption("1", "--slippage")).toBe(1)
3658
expect(parseFloatOption("0.01", "--slippage")).toBe(0.01)
59+
expect(parseFloatOption(".5", "--slippage")).toBe(0.5)
60+
expect(parseFloatOption("1.", "--slippage")).toBe(1)
61+
expect(parseFloatOption("1e-2", "--slippage")).toBe(0.01)
3762
})
3863

3964
it("throws on non-numeric strings", () => {
@@ -47,6 +72,18 @@ describe("parseFloatOption", () => {
4772
'Invalid value for --slippage: "" is not a number',
4873
)
4974
})
75+
76+
it.each([
77+
"0.5oops",
78+
"0.01%",
79+
"Infinity",
80+
"1e309",
81+
"1.5\n",
82+
])('throws when "%s" is not an entirely finite number', value => {
83+
expect(() => parseFloatOption(value, "--slippage")).toThrow(
84+
`Invalid value for --slippage: "${value}" is not a number`,
85+
)
86+
})
5087
})
5188

5289
describe("readJsonBodyOption", () => {

0 commit comments

Comments
 (0)