Skip to content

Commit 6b194ed

Browse files
committed
Release v1.8.0
Origin-SHA: 36691654b9a5b5cbfe770b3d23c8fc9f05179a62
1 parent a50054e commit 6b194ed

22 files changed

Lines changed: 391 additions & 1061 deletions

.agents/rules.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ CLI (src/cli.ts) SDK (src/sdk.ts)
4949
| `src/sdk.ts` | High-level SDK with domain-specific API classes (`OpenSeaCLI`). |
5050
| `src/cli.ts` | CLI entrypoint. Configures Commander program and global options. |
5151
| `src/commands/*.ts` | One file per API domain. Each exports a factory function. |
52-
| `src/output.ts` | Output formatting (JSON or table). |
52+
| `src/output.ts` | Output formatting (JSON, table, or server-side TOON via `Accept: text/markdown`). |
53+
| `src/parse.ts` | Shared `--body <path>` JSON file reading utility. |
54+
| `src/health.ts` | Health-check logic used by the `health` command. |
55+
| `src/wallet/` | Wallet adapter re-exports from `@opensea/wallet-adapters` and chain ID resolution. |
5356
| `src/types/api.ts` | TypeScript interfaces matching OpenSea API v2 response shapes. |
5457
| `src/types/index.ts` | Re-exports API types plus internal config types. |
5558

@@ -68,6 +71,7 @@ CI runs `pnpm check-api-paths` on every PR; it fails if `src/sdk.ts` or `src/com
6871
Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK class (`src/sdk.ts`):
6972

7073
- **accounts** - Account profile lookup
74+
- **assets** - Asset movement transactions (transfers)
7175
- **auth** - API key request and management
7276
- **chains** - Chain information and supported networks
7377
- **collections** - Collection metadata, stats, traits
@@ -218,7 +222,14 @@ npm run test -- --coverage # Run with coverage report
218222
| `test/client.test.ts` | `OpenSeaClient` (get, post, graphql, error handling) |
219223
| `test/output.test.ts` | `formatOutput` (JSON and table formatting) |
220224
| `test/sdk.test.ts` | All SDK API classes and their methods |
225+
| `test/sdk-swaps.test.ts` | SDK swap-specific tests |
221226
| `test/commands/*.test.ts` | Each CLI command module (option parsing, subcommand routing, output) |
227+
| `test/cli-api-error.test.ts` | CLI API error handling and exit codes |
228+
| `test/cli-network-error.test.ts` | CLI network error handling |
229+
| `test/cli-rate-limit.test.ts` | CLI rate-limit (HTTP 429) handling |
230+
| `test/parse.test.ts` | `--body` JSON file parsing utility |
231+
| `test/toon.test.ts` | Toon output formatter |
232+
| `test/resolve-quantity.test.ts` | Quantity resolution logic |
222233
| `test/integration.test.ts` | End-to-end SDK flows with mocked `fetch` |
223234
| `test/mocks.ts` | Shared mock factories (`createCommandTestContext`, `mockFetchResponse`, `mockFetchTextResponse`) |
224235

CHANGELOG.md

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

3+
## 1.8.0
4+
5+
### Minor Changes
6+
7+
- 0bc1053: Source `EventAsset`, `AssetEvent`, `Token`, `TokenDetails`, `TokenStats`, and `TokenSocials` from `@opensea/api-types` instead of hand-rolling them.
8+
9+
## What changed
10+
11+
- `EventAsset` is now an alias for the api-types `Nft` schema. It gains `display_image_url`, `display_animation_url`, `original_image_url`, `original_animation_url`, and `traits` fields, and relaxes `name`, `description`, `image_url`, and `metadata_url` from required to optional (matching the OpenAPI spec — these are still present in every live response we probed).
12+
- `AssetEvent` is now `AssetEventsResponse["asset_events"][number]`, i.e. the `OrderEvent | SaleEvent | TransferEvent` union from the spec. Consumers can now narrow on `event_type` to access variant-specific fields (`seller`, `buyer`, `nft` on sales; `transfer_type`, `from_address`, `to_address` on transfers; `order_type`, `asset`, `maker`, `taker` on orders). The previous `[key: string]: unknown` index signature is gone; code that read arbitrary fields off an `AssetEvent` will need to narrow first or cast.
13+
- `Token`, `TokenDetails`, `TokenStats`, `TokenSocials` are now aliases for `TokenResponse`, `TokenDetailedResponse`, `TokenStatsResponse`, `TokenSocialsResponse`. Field shapes are identical except `TokenDetails` gains an optional `status` field (`"OK" | "WARNING" | "SPAM" | "LOW_LIQUIDITY"`) that the live API has been returning.
14+
- `ChainInfo` / `ChainListResponse` are now aliases for the api-types `ChainResponse` / `ChainListResponse`. Identical shape.
15+
- `TokenBalance` / `TokenBalancePaginatedResponse` are now aliases for the api-types `TokenBalanceResponse` / `TokenBalancePaginatedResponse`. `TokenBalance` gains optional `status`, `base_token_liquidity_usd`, and `quote_token_liquidity_usd` fields that the live API returns.
16+
- `SearchResultCollection` / `SearchResultToken` / `SearchResultNFT` / `SearchResultAccount` / `SearchResult` / `SearchResponse` are now aliases for api-types `CollectionSearchResponse` / `TokenSearchResponse` / `NftSearchResponse` / `AccountSearchResponse` / `SearchResultResponse` / `SearchResponse`. Field shapes match.
17+
- `SwapQuote` / `SwapTransaction` / `SwapQuoteResponse` are now aliases for api-types `SwapQuoteDetails` / `SwapTransactionResponse` / `SwapQuoteResponse`. `SwapQuote` gains optional `price_impact`, `swap_provider`, and required `costs` / `route_errors` fields.
18+
19+
## Migration
20+
21+
Most consumers won't need any changes — the same snake_case fields are still there. Code that did `event.someArbitraryField` will need to narrow on `event_type` first:
22+
23+
```ts
24+
// Before
25+
const seller = event.seller as string;
26+
27+
// After
28+
if (event.event_type === "sale") {
29+
const seller = event.seller; // typed as string
30+
}
31+
```
32+
33+
- a10c5c0: Switch `--format toon` to server-side TOON encoding via `Accept: text/markdown` content negotiation. The client-side encoder (`src/toon.ts` and `formatToon`) is gone now that os2-core supports TOON encoding server-side. `--format toon` still works — it just triggers a `getAsMarkdown` call instead of running a 338-line encoder client-side.
34+
35+
**Note:** `formatToon` is no longer exported from `@opensea/cli`. The CLI doesn't depend on `@opensea/sdk` directly (responses pass through `outputGet`'s generic JSON formatter), so the SDK 11.0 shape changes don't affect CLI output.
36+
37+
### Patch Changes
38+
39+
- Updated dependencies [fb03c09]
40+
- @opensea/api-types@0.4.2
41+
342
## 1.7.0
443

544
### Minor Changes

README.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,7 @@ tokens[3]{name,symbol,chain,market_cap,price_usd}:
154154
next: abc123
155155
```
156156

157-
TOON collapses uniform arrays of objects into CSV-like tables with a single header row, while nested objects use YAML-like indentation. The encoder follows the [TOON v3.0 spec](https://github.com/toon-format/spec/blob/main/SPEC.md) and is implemented without external dependencies.
158-
159-
TOON is also available programmatically via the SDK:
160-
161-
```typescript
162-
import { formatToon } from "@opensea/cli"
163-
164-
const data = await client.tokens.trending({ limit: 5 })
165-
console.log(formatToon(data))
166-
```
157+
TOON collapses uniform arrays of objects into CSV-like tables with a single header row, while nested objects use YAML-like indentation. The encoding is performed server-side via the `Accept: text/markdown` header — no client-side encoder is needed.
167158

168159
## Exit Codes
169160

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "1.7.0",
3+
"version": "1.8.0",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -23,7 +23,7 @@
2323
"prepublishOnly": "npm run build"
2424
},
2525
"dependencies": {
26-
"@opensea/api-types": "^0.4.0",
26+
"@opensea/api-types": "^0.4.2",
2727
"@opensea/wallet-adapters": "^0.3.0",
2828
"commander": "^14.0.3",
2929
"zod": "^4.3.6"

src/client.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,41 @@ export class OpenSeaClient {
8080
return response.json() as Promise<T>
8181
}
8282

83+
async getAsMarkdown(
84+
path: string,
85+
params?: Record<string, unknown>,
86+
): Promise<{ text: string; isMarkdown: boolean }> {
87+
const url = new URL(`${this.baseUrl}${path}`)
88+
89+
if (params) {
90+
for (const [key, value] of Object.entries(params)) {
91+
if (value !== undefined && value !== null) {
92+
url.searchParams.set(key, String(value))
93+
}
94+
}
95+
}
96+
97+
if (this.verbose) {
98+
console.error(`[verbose] GET ${url.toString()} (Accept: text/markdown)`)
99+
}
100+
101+
const response = await this.fetchWithRetry(
102+
url.toString(),
103+
{
104+
method: "GET",
105+
headers: { ...this.defaultHeaders, Accept: "text/markdown" },
106+
},
107+
path,
108+
)
109+
110+
const contentType = response.headers.get("content-type") ?? ""
111+
const text = await response.text()
112+
return {
113+
text,
114+
isMarkdown: contentType.includes("text/markdown"),
115+
}
116+
}
117+
83118
async post<T>(
84119
path: string,
85120
body?: Record<string, unknown>,

src/commands/accounts.ts

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
import { Command } from "commander"
22
import type { OpenSeaClient } from "../client.js"
33
import type { OutputFormat } from "../output.js"
4-
import { formatOutput } from "../output.js"
4+
import { outputGet } from "../output.js"
55
import { parseIntOption } from "../parse.js"
6-
import type {
7-
Account,
8-
AccountResolveResponse,
9-
Listing,
10-
NFT,
11-
Offer,
12-
PortfolioHistoryResponse,
13-
PortfolioStatsResponse,
14-
ProfileCollectionsResponse,
15-
TokenBalancePaginatedResponse,
16-
TokenBalanceSortBy,
17-
} from "../types/index.js"
6+
import type { TokenBalanceSortBy } from "../types/index.js"
187

198
export function accountsCommand(
209
getClient: () => OpenSeaClient,
@@ -28,8 +17,7 @@ export function accountsCommand(
2817
.argument("<address>", "Wallet address")
2918
.action(async (address: string) => {
3019
const client = getClient()
31-
const result = await client.get<Account>(`/api/v2/accounts/${address}`)
32-
console.log(formatOutput(result, getFormat()))
20+
await outputGet(client, getFormat(), `/api/v2/accounts/${address}`)
3321
})
3422

3523
cmd
@@ -58,7 +46,9 @@ export function accountsCommand(
5846
},
5947
) => {
6048
const client = getClient()
61-
const result = await client.get<TokenBalancePaginatedResponse>(
49+
await outputGet(
50+
client,
51+
getFormat(),
6252
`/api/v2/account/${address}/tokens`,
6353
{
6454
chains: options.chains,
@@ -69,7 +59,6 @@ export function accountsCommand(
6959
disable_spam_filtering: options.spamFilter ? undefined : true,
7060
},
7161
)
72-
console.log(formatOutput(result, getFormat()))
7362
},
7463
)
7564

@@ -84,10 +73,11 @@ export function accountsCommand(
8473
)
8574
.action(async (identifier: string) => {
8675
const client = getClient()
87-
const result = await client.get<AccountResolveResponse>(
76+
await outputGet(
77+
client,
78+
getFormat(),
8879
`/api/v2/accounts/resolve/${identifier}`,
8980
)
90-
console.log(formatOutput(result, getFormat()))
9181
})
9282

9383
cmd
@@ -97,11 +87,12 @@ export function accountsCommand(
9787
.option("--timeframe <window>", "P&L window (HOUR, DAY, WEEK, MONTH)")
9888
.action(async (address: string, options: { timeframe?: string }) => {
9989
const client = getClient()
100-
const result = await client.get<PortfolioStatsResponse>(
90+
await outputGet(
91+
client,
92+
getFormat(),
10193
`/api/v2/account/${address}/portfolio`,
10294
{ timeframe: options.timeframe },
10395
)
104-
console.log(formatOutput(result, getFormat()))
10596
})
10697

10798
cmd
@@ -111,11 +102,12 @@ export function accountsCommand(
111102
.option("--timeframe <window>", "History window (HOUR, DAY, WEEK, MONTH)")
112103
.action(async (address: string, options: { timeframe?: string }) => {
113104
const client = getClient()
114-
const result = await client.get<PortfolioHistoryResponse>(
105+
await outputGet(
106+
client,
107+
getFormat(),
115108
`/api/v2/account/${address}/portfolio/history`,
116109
{ timeframe: options.timeframe },
117110
)
118-
console.log(formatOutput(result, getFormat()))
119111
})
120112

121113
cmd
@@ -144,7 +136,9 @@ export function accountsCommand(
144136
},
145137
) => {
146138
const client = getClient()
147-
const result = await client.get<{ offers: Offer[]; next?: string }>(
139+
await outputGet(
140+
client,
141+
getFormat(),
148142
`/api/v2/account/${address}/offers`,
149143
{
150144
after: options.after,
@@ -155,7 +149,6 @@ export function accountsCommand(
155149
sort_direction: options.sortDirection,
156150
},
157151
)
158-
console.log(formatOutput(result, getFormat()))
159152
},
160153
)
161154

@@ -185,7 +178,9 @@ export function accountsCommand(
185178
},
186179
) => {
187180
const client = getClient()
188-
const result = await client.get<{ offers: Offer[]; next?: string }>(
181+
await outputGet(
182+
client,
183+
getFormat(),
189184
`/api/v2/account/${address}/offers_received`,
190185
{
191186
after: options.after,
@@ -196,7 +191,6 @@ export function accountsCommand(
196191
sort_direction: options.sortDirection,
197192
},
198193
)
199-
console.log(formatOutput(result, getFormat()))
200194
},
201195
)
202196

@@ -226,18 +220,19 @@ export function accountsCommand(
226220
},
227221
) => {
228222
const client = getClient()
229-
const result = await client.get<{
230-
listings: Listing[]
231-
next?: string
232-
}>(`/api/v2/account/${address}/listings`, {
233-
after: options.after,
234-
limit: parseIntOption(options.limit, "--limit"),
235-
collection_slugs: options.collectionSlugs,
236-
chains: options.chains,
237-
sort_by: options.sortBy,
238-
sort_direction: options.sortDirection,
239-
})
240-
console.log(formatOutput(result, getFormat()))
223+
await outputGet(
224+
client,
225+
getFormat(),
226+
`/api/v2/account/${address}/listings`,
227+
{
228+
after: options.after,
229+
limit: parseIntOption(options.limit, "--limit"),
230+
collection_slugs: options.collectionSlugs,
231+
chains: options.chains,
232+
sort_by: options.sortBy,
233+
sort_direction: options.sortDirection,
234+
},
235+
)
241236
},
242237
)
243238

@@ -262,7 +257,9 @@ export function accountsCommand(
262257
},
263258
) => {
264259
const client = getClient()
265-
const result = await client.get<{ nfts: NFT[]; next?: string }>(
260+
await outputGet(
261+
client,
262+
getFormat(),
266263
`/api/v2/account/${address}/favorites`,
267264
{
268265
after: options.after,
@@ -272,7 +269,6 @@ export function accountsCommand(
272269
chains: options.chains,
273270
},
274271
)
275-
console.log(formatOutput(result, getFormat()))
276272
},
277273
)
278274

@@ -293,15 +289,16 @@ export function accountsCommand(
293289
},
294290
) => {
295291
const client = getClient()
296-
const result = await client.get<ProfileCollectionsResponse>(
292+
await outputGet(
293+
client,
294+
getFormat(),
297295
`/api/v2/account/${address}/collections`,
298296
{
299297
after: options.after,
300298
limit: parseIntOption(options.limit, "--limit"),
301299
chains: options.chains,
302300
},
303301
)
304-
console.log(formatOutput(result, getFormat()))
305302
},
306303
)
307304

src/commands/chains.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Command } from "commander"
22
import type { OpenSeaClient } from "../client.js"
33
import type { OutputFormat } from "../output.js"
4-
import { formatOutput } from "../output.js"
5-
import type { ChainListResponse } from "../types/index.js"
4+
import { outputGet } from "../output.js"
65

76
export function chainsCommand(
87
getClient: () => OpenSeaClient,
@@ -15,8 +14,7 @@ export function chainsCommand(
1514
.description("List all supported blockchains and their capabilities")
1615
.action(async () => {
1716
const client = getClient()
18-
const result = await client.get<ChainListResponse>("/api/v2/chains")
19-
console.log(formatOutput(result, getFormat()))
17+
await outputGet(client, getFormat(), "/api/v2/chains")
2018
})
2119

2220
return cmd

0 commit comments

Comments
 (0)