@opensea/cli 0.4.0 → 0.4.2
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 +2 -1
- package/dist/cli.js +265 -226
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +62 -90
- package/dist/index.js +195 -189
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/client.ts
|
|
7
7
|
var DEFAULT_BASE_URL = "https://api.opensea.io";
|
|
8
|
-
var DEFAULT_GRAPHQL_URL = "https://gql.opensea.io/graphql";
|
|
9
8
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
9
|
+
var USER_AGENT = `opensea-cli/${"0.4.2"}`;
|
|
10
|
+
var DEFAULT_MAX_RETRIES = 0;
|
|
11
|
+
var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
|
|
12
|
+
function isRetryableStatus(status, method) {
|
|
13
|
+
if (status === 429) return true;
|
|
14
|
+
return status >= 500 && method === "GET";
|
|
15
|
+
}
|
|
16
|
+
function parseRetryAfter(header) {
|
|
17
|
+
if (!header) return void 0;
|
|
18
|
+
const seconds = Number(header);
|
|
19
|
+
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
20
|
+
const date = Date.parse(header);
|
|
21
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
10
27
|
var OpenSeaClient = class {
|
|
11
28
|
apiKey;
|
|
12
29
|
baseUrl;
|
|
13
|
-
graphqlUrl;
|
|
14
30
|
defaultChain;
|
|
15
31
|
timeoutMs;
|
|
16
32
|
verbose;
|
|
33
|
+
maxRetries;
|
|
34
|
+
retryBaseDelay;
|
|
17
35
|
constructor(config) {
|
|
18
36
|
this.apiKey = config.apiKey;
|
|
19
37
|
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
20
|
-
this.graphqlUrl = config.graphqlUrl ?? DEFAULT_GRAPHQL_URL;
|
|
21
38
|
this.defaultChain = config.chain ?? "ethereum";
|
|
22
39
|
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
23
40
|
this.verbose = config.verbose ?? false;
|
|
41
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
42
|
+
this.retryBaseDelay = config.retryBaseDelay ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
43
|
+
}
|
|
44
|
+
get defaultHeaders() {
|
|
45
|
+
return {
|
|
46
|
+
Accept: "application/json",
|
|
47
|
+
"User-Agent": USER_AGENT,
|
|
48
|
+
"x-api-key": this.apiKey
|
|
49
|
+
};
|
|
24
50
|
}
|
|
25
51
|
async get(path, params) {
|
|
26
52
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
@@ -34,21 +60,14 @@ var OpenSeaClient = class {
|
|
|
34
60
|
if (this.verbose) {
|
|
35
61
|
console.error(`[verbose] GET ${url.toString()}`);
|
|
36
62
|
}
|
|
37
|
-
const response = await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
const response = await this.fetchWithRetry(
|
|
64
|
+
url.toString(),
|
|
65
|
+
{
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: this.defaultHeaders
|
|
42
68
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (this.verbose) {
|
|
46
|
-
console.error(`[verbose] ${response.status} ${response.statusText}`);
|
|
47
|
-
}
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
const body = await response.text();
|
|
50
|
-
throw new OpenSeaAPIError(response.status, body, path);
|
|
51
|
-
}
|
|
69
|
+
path
|
|
70
|
+
);
|
|
52
71
|
return response.json();
|
|
53
72
|
}
|
|
54
73
|
async post(path, body, params) {
|
|
@@ -60,68 +79,67 @@ var OpenSeaClient = class {
|
|
|
60
79
|
}
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
|
-
const headers = {
|
|
64
|
-
Accept: "application/json",
|
|
65
|
-
"x-api-key": this.apiKey
|
|
66
|
-
};
|
|
82
|
+
const headers = { ...this.defaultHeaders };
|
|
67
83
|
if (body) {
|
|
68
84
|
headers["Content-Type"] = "application/json";
|
|
69
85
|
}
|
|
70
86
|
if (this.verbose) {
|
|
71
87
|
console.error(`[verbose] POST ${url.toString()}`);
|
|
72
88
|
}
|
|
73
|
-
const response = await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (this.verbose) {
|
|
80
|
-
console.error(`[verbose] ${response.status} ${response.statusText}`);
|
|
81
|
-
}
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
const text = await response.text();
|
|
84
|
-
throw new OpenSeaAPIError(response.status, text, path);
|
|
85
|
-
}
|
|
86
|
-
return response.json();
|
|
87
|
-
}
|
|
88
|
-
async graphql(query, variables) {
|
|
89
|
-
if (this.verbose) {
|
|
90
|
-
console.error(`[verbose] POST ${this.graphqlUrl}`);
|
|
91
|
-
}
|
|
92
|
-
const response = await fetch(this.graphqlUrl, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: {
|
|
95
|
-
"Content-Type": "application/json",
|
|
96
|
-
Accept: "application/json",
|
|
97
|
-
"x-api-key": this.apiKey
|
|
89
|
+
const response = await this.fetchWithRetry(
|
|
90
|
+
url.toString(),
|
|
91
|
+
{
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers,
|
|
94
|
+
body: body ? JSON.stringify(body) : void 0
|
|
98
95
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (this.verbose) {
|
|
103
|
-
console.error(`[verbose] ${response.status} ${response.statusText}`);
|
|
104
|
-
}
|
|
105
|
-
if (!response.ok) {
|
|
106
|
-
const body = await response.text();
|
|
107
|
-
throw new OpenSeaAPIError(response.status, body, "graphql");
|
|
108
|
-
}
|
|
109
|
-
const json = await response.json();
|
|
110
|
-
if (json.errors?.length) {
|
|
111
|
-
throw new OpenSeaAPIError(
|
|
112
|
-
400,
|
|
113
|
-
json.errors.map((e) => e.message).join("; "),
|
|
114
|
-
"graphql"
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (!json.data) {
|
|
118
|
-
throw new OpenSeaAPIError(500, "GraphQL response missing data", "graphql");
|
|
119
|
-
}
|
|
120
|
-
return json.data;
|
|
96
|
+
path
|
|
97
|
+
);
|
|
98
|
+
return response.json();
|
|
121
99
|
}
|
|
122
100
|
getDefaultChain() {
|
|
123
101
|
return this.defaultChain;
|
|
124
102
|
}
|
|
103
|
+
getApiKeyPrefix() {
|
|
104
|
+
if (this.apiKey.length < 8) return "***";
|
|
105
|
+
return `${this.apiKey.slice(0, 4)}...`;
|
|
106
|
+
}
|
|
107
|
+
async fetchWithRetry(url, init, path) {
|
|
108
|
+
for (let attempt = 0; ; attempt++) {
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
...init,
|
|
111
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
112
|
+
});
|
|
113
|
+
if (this.verbose) {
|
|
114
|
+
console.error(`[verbose] ${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
if (response.ok) {
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
const method = init.method ?? "GET";
|
|
120
|
+
if (attempt < this.maxRetries && isRetryableStatus(response.status, method)) {
|
|
121
|
+
const retryAfterMs = parseRetryAfter(
|
|
122
|
+
response.headers.get("Retry-After")
|
|
123
|
+
);
|
|
124
|
+
const backoffMs = this.retryBaseDelay * 2 ** attempt;
|
|
125
|
+
const jitterMs = Math.random() * this.retryBaseDelay;
|
|
126
|
+
const delayMs = Math.max(retryAfterMs ?? 0, backoffMs) + jitterMs;
|
|
127
|
+
if (this.verbose) {
|
|
128
|
+
console.error(
|
|
129
|
+
`[verbose] Retry ${attempt + 1}/${this.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await response.body?.cancel();
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
await sleep(delayMs);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const text = await response.text();
|
|
140
|
+
throw new OpenSeaAPIError(response.status, text, path);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
125
143
|
};
|
|
126
144
|
var OpenSeaAPIError = class extends Error {
|
|
127
145
|
constructor(statusCode, responseBody, path) {
|
|
@@ -407,14 +425,24 @@ function formatToon(data) {
|
|
|
407
425
|
}
|
|
408
426
|
|
|
409
427
|
// src/output.ts
|
|
428
|
+
var _outputOptions = {};
|
|
429
|
+
function setOutputOptions(options) {
|
|
430
|
+
_outputOptions = options;
|
|
431
|
+
}
|
|
410
432
|
function formatOutput(data, format) {
|
|
433
|
+
const processed = _outputOptions.fields ? filterFields(data, _outputOptions.fields) : data;
|
|
434
|
+
let result;
|
|
411
435
|
if (format === "table") {
|
|
412
|
-
|
|
436
|
+
result = formatTable(processed);
|
|
437
|
+
} else if (format === "toon") {
|
|
438
|
+
result = formatToon(processed);
|
|
439
|
+
} else {
|
|
440
|
+
result = JSON.stringify(processed, null, 2);
|
|
413
441
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
442
|
+
if (_outputOptions.maxLines != null) {
|
|
443
|
+
result = truncateOutput(result, _outputOptions.maxLines);
|
|
416
444
|
}
|
|
417
|
-
return
|
|
445
|
+
return result;
|
|
418
446
|
}
|
|
419
447
|
function formatTable(data) {
|
|
420
448
|
if (Array.isArray(data)) {
|
|
@@ -450,6 +478,31 @@ function formatTable(data) {
|
|
|
450
478
|
}
|
|
451
479
|
return String(data);
|
|
452
480
|
}
|
|
481
|
+
function pickFields(obj, fields) {
|
|
482
|
+
const result = {};
|
|
483
|
+
for (const field of fields) {
|
|
484
|
+
if (field in obj) {
|
|
485
|
+
result[field] = obj[field];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
function filterFields(data, fields) {
|
|
491
|
+
if (Array.isArray(data)) {
|
|
492
|
+
return data.map((item) => filterFields(item, fields));
|
|
493
|
+
}
|
|
494
|
+
if (data && typeof data === "object") {
|
|
495
|
+
return pickFields(data, fields);
|
|
496
|
+
}
|
|
497
|
+
return data;
|
|
498
|
+
}
|
|
499
|
+
function truncateOutput(text, maxLines) {
|
|
500
|
+
const lines = text.split("\n");
|
|
501
|
+
if (lines.length <= maxLines) return text;
|
|
502
|
+
const omitted = lines.length - maxLines;
|
|
503
|
+
return lines.slice(0, maxLines).join("\n") + `
|
|
504
|
+
... (${omitted} more line${omitted === 1 ? "" : "s"})`;
|
|
505
|
+
}
|
|
453
506
|
|
|
454
507
|
// src/commands/accounts.ts
|
|
455
508
|
function accountsCommand(getClient2, getFormat2) {
|
|
@@ -586,10 +639,88 @@ function eventsCommand(getClient2, getFormat2) {
|
|
|
586
639
|
return cmd;
|
|
587
640
|
}
|
|
588
641
|
|
|
589
|
-
// src/commands/
|
|
642
|
+
// src/commands/health.ts
|
|
590
643
|
import { Command as Command4 } from "commander";
|
|
644
|
+
|
|
645
|
+
// src/health.ts
|
|
646
|
+
async function checkHealth(client) {
|
|
647
|
+
const keyPrefix = client.getApiKeyPrefix();
|
|
648
|
+
try {
|
|
649
|
+
await client.get("/api/v2/collections", { limit: 1 });
|
|
650
|
+
} catch (error) {
|
|
651
|
+
let message;
|
|
652
|
+
if (error instanceof OpenSeaAPIError) {
|
|
653
|
+
message = error.statusCode === 429 ? "Rate limited: too many requests" : `API error (${error.statusCode}): ${error.responseBody}`;
|
|
654
|
+
} else {
|
|
655
|
+
message = `Network error: ${error.message}`;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
status: "error",
|
|
659
|
+
key_prefix: keyPrefix,
|
|
660
|
+
authenticated: false,
|
|
661
|
+
rate_limited: error instanceof OpenSeaAPIError && error.statusCode === 429,
|
|
662
|
+
message
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await client.get("/api/v2/listings/collection/boredapeyachtclub/all", {
|
|
667
|
+
limit: 1
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
status: "ok",
|
|
671
|
+
key_prefix: keyPrefix,
|
|
672
|
+
authenticated: true,
|
|
673
|
+
rate_limited: false,
|
|
674
|
+
message: "Connectivity and authentication are working"
|
|
675
|
+
};
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error instanceof OpenSeaAPIError) {
|
|
678
|
+
if (error.statusCode === 429) {
|
|
679
|
+
return {
|
|
680
|
+
status: "error",
|
|
681
|
+
key_prefix: keyPrefix,
|
|
682
|
+
authenticated: false,
|
|
683
|
+
rate_limited: true,
|
|
684
|
+
message: "Rate limited: too many requests"
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (error.statusCode === 401 || error.statusCode === 403) {
|
|
688
|
+
return {
|
|
689
|
+
status: "error",
|
|
690
|
+
key_prefix: keyPrefix,
|
|
691
|
+
authenticated: false,
|
|
692
|
+
rate_limited: false,
|
|
693
|
+
message: `Authentication failed (${error.statusCode}): invalid API key`
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
status: "ok",
|
|
699
|
+
key_prefix: keyPrefix,
|
|
700
|
+
authenticated: false,
|
|
701
|
+
rate_limited: false,
|
|
702
|
+
message: "Connectivity is working but authentication could not be verified"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/commands/health.ts
|
|
708
|
+
function healthCommand(getClient2, getFormat2) {
|
|
709
|
+
const cmd = new Command4("health").description("Check API connectivity and authentication").action(async () => {
|
|
710
|
+
const client = getClient2();
|
|
711
|
+
const result = await checkHealth(client);
|
|
712
|
+
console.log(formatOutput(result, getFormat2()));
|
|
713
|
+
if (result.status === "error") {
|
|
714
|
+
process.exit(result.rate_limited ? 3 : 1);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
return cmd;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/listings.ts
|
|
721
|
+
import { Command as Command5 } from "commander";
|
|
591
722
|
function listingsCommand(getClient2, getFormat2) {
|
|
592
|
-
const cmd = new
|
|
723
|
+
const cmd = new Command5("listings").description("Query NFT listings");
|
|
593
724
|
cmd.command("all").description("Get all listings for a collection").argument("<collection>", "Collection slug").option("--limit <limit>", "Number of results", "20").option("--next <cursor>", "Pagination cursor").action(
|
|
594
725
|
async (collection, options) => {
|
|
595
726
|
const client = getClient2();
|
|
@@ -621,9 +752,9 @@ function listingsCommand(getClient2, getFormat2) {
|
|
|
621
752
|
}
|
|
622
753
|
|
|
623
754
|
// src/commands/nfts.ts
|
|
624
|
-
import { Command as
|
|
755
|
+
import { Command as Command6 } from "commander";
|
|
625
756
|
function nftsCommand(getClient2, getFormat2) {
|
|
626
|
-
const cmd = new
|
|
757
|
+
const cmd = new Command6("nfts").description("Query NFTs");
|
|
627
758
|
cmd.command("get").description("Get a single NFT").argument("<chain>", "Chain (e.g. ethereum, base)").argument("<contract>", "Contract address").argument("<token-id>", "Token ID").action(async (chain, contract, tokenId) => {
|
|
628
759
|
const client = getClient2();
|
|
629
760
|
const result = await client.get(
|
|
@@ -691,9 +822,9 @@ function nftsCommand(getClient2, getFormat2) {
|
|
|
691
822
|
}
|
|
692
823
|
|
|
693
824
|
// src/commands/offers.ts
|
|
694
|
-
import { Command as
|
|
825
|
+
import { Command as Command7 } from "commander";
|
|
695
826
|
function offersCommand(getClient2, getFormat2) {
|
|
696
|
-
const cmd = new
|
|
827
|
+
const cmd = new Command7("offers").description("Query NFT offers");
|
|
697
828
|
cmd.command("all").description("Get all offers for a collection").argument("<collection>", "Collection slug").option("--limit <limit>", "Number of results", "20").option("--next <cursor>", "Pagination cursor").action(
|
|
698
829
|
async (collection, options) => {
|
|
699
830
|
const client = getClient2();
|
|
@@ -737,155 +868,38 @@ function offersCommand(getClient2, getFormat2) {
|
|
|
737
868
|
}
|
|
738
869
|
|
|
739
870
|
// src/commands/search.ts
|
|
740
|
-
import { Command as
|
|
741
|
-
|
|
742
|
-
// src/queries.ts
|
|
743
|
-
var SEARCH_COLLECTIONS_QUERY = `
|
|
744
|
-
query SearchCollections($query: String!, $limit: Int, $chains: [ChainIdentifier!]) {
|
|
745
|
-
collectionsByQuery(query: $query, limit: $limit, chains: $chains) {
|
|
746
|
-
slug
|
|
747
|
-
name
|
|
748
|
-
description
|
|
749
|
-
imageUrl
|
|
750
|
-
chain {
|
|
751
|
-
identifier
|
|
752
|
-
name
|
|
753
|
-
}
|
|
754
|
-
stats {
|
|
755
|
-
totalSupply
|
|
756
|
-
ownerCount
|
|
757
|
-
volume {
|
|
758
|
-
usd
|
|
759
|
-
}
|
|
760
|
-
sales
|
|
761
|
-
}
|
|
762
|
-
floorPrice {
|
|
763
|
-
pricePerItem {
|
|
764
|
-
usd
|
|
765
|
-
native {
|
|
766
|
-
unit
|
|
767
|
-
symbol
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}`;
|
|
773
|
-
var SEARCH_NFTS_QUERY = `
|
|
774
|
-
query SearchItems($query: String!, $collectionSlug: String, $limit: Int, $chains: [ChainIdentifier!]) {
|
|
775
|
-
itemsByQuery(query: $query, collectionSlug: $collectionSlug, limit: $limit, chains: $chains) {
|
|
776
|
-
tokenId
|
|
777
|
-
name
|
|
778
|
-
description
|
|
779
|
-
imageUrl
|
|
780
|
-
contractAddress
|
|
781
|
-
collection {
|
|
782
|
-
slug
|
|
783
|
-
name
|
|
784
|
-
}
|
|
785
|
-
chain {
|
|
786
|
-
identifier
|
|
787
|
-
name
|
|
788
|
-
}
|
|
789
|
-
bestListing {
|
|
790
|
-
pricePerItem {
|
|
791
|
-
usd
|
|
792
|
-
native {
|
|
793
|
-
unit
|
|
794
|
-
symbol
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
owner {
|
|
799
|
-
address
|
|
800
|
-
displayName
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}`;
|
|
804
|
-
var SEARCH_TOKENS_QUERY = `
|
|
805
|
-
query SearchCurrencies($query: String!, $limit: Int, $chain: ChainIdentifier) {
|
|
806
|
-
currenciesByQuery(query: $query, limit: $limit, chain: $chain, allowlistOnly: false) {
|
|
807
|
-
name
|
|
808
|
-
symbol
|
|
809
|
-
imageUrl
|
|
810
|
-
usdPrice
|
|
811
|
-
contractAddress
|
|
812
|
-
chain {
|
|
813
|
-
identifier
|
|
814
|
-
name
|
|
815
|
-
}
|
|
816
|
-
stats {
|
|
817
|
-
marketCapUsd
|
|
818
|
-
oneDay {
|
|
819
|
-
priceChange
|
|
820
|
-
volume
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}`;
|
|
825
|
-
var SEARCH_ACCOUNTS_QUERY = `
|
|
826
|
-
query SearchAccounts($query: String!, $limit: Int) {
|
|
827
|
-
accountsByQuery(query: $query, limit: $limit) {
|
|
828
|
-
address
|
|
829
|
-
username
|
|
830
|
-
imageUrl
|
|
831
|
-
isVerified
|
|
832
|
-
}
|
|
833
|
-
}`;
|
|
834
|
-
|
|
835
|
-
// src/commands/search.ts
|
|
871
|
+
import { Command as Command8 } from "commander";
|
|
836
872
|
function searchCommand(getClient2, getFormat2) {
|
|
837
|
-
const cmd = new
|
|
838
|
-
"
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
async (query, options) => {
|
|
842
|
-
const client = getClient2();
|
|
843
|
-
const result = await client.graphql(SEARCH_COLLECTIONS_QUERY, {
|
|
844
|
-
query,
|
|
845
|
-
limit: parseIntOption(options.limit, "--limit"),
|
|
846
|
-
chains: options.chains?.split(",")
|
|
847
|
-
});
|
|
848
|
-
console.log(formatOutput(result.collectionsByQuery, getFormat2()));
|
|
849
|
-
}
|
|
850
|
-
);
|
|
851
|
-
cmd.command("nfts").description("Search NFTs by name").argument("<query>", "Search query").option("--collection <slug>", "Filter by collection slug").option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "10").action(
|
|
852
|
-
async (query, options) => {
|
|
853
|
-
const client = getClient2();
|
|
854
|
-
const result = await client.graphql(SEARCH_NFTS_QUERY, {
|
|
855
|
-
query,
|
|
856
|
-
collectionSlug: options.collection,
|
|
857
|
-
limit: parseIntOption(options.limit, "--limit"),
|
|
858
|
-
chains: options.chains?.split(",")
|
|
859
|
-
});
|
|
860
|
-
console.log(formatOutput(result.itemsByQuery, getFormat2()));
|
|
861
|
-
}
|
|
862
|
-
);
|
|
863
|
-
cmd.command("tokens").description("Search tokens/currencies by name or symbol").argument("<query>", "Search query").option("--chain <chain>", "Filter by chain").option("--limit <limit>", "Number of results", "10").action(
|
|
873
|
+
const cmd = new Command8("search").description("Search across collections, tokens, NFTs, and accounts").argument("<query>", "Search query").option(
|
|
874
|
+
"--types <types>",
|
|
875
|
+
"Filter by type (comma-separated: collection,nft,token,account)"
|
|
876
|
+
).option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "20").action(
|
|
864
877
|
async (query, options) => {
|
|
865
878
|
const client = getClient2();
|
|
866
|
-
const
|
|
879
|
+
const params = {
|
|
867
880
|
query,
|
|
868
|
-
limit: parseIntOption(options.limit, "--limit")
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
881
|
+
limit: parseIntOption(options.limit, "--limit")
|
|
882
|
+
};
|
|
883
|
+
if (options.types) {
|
|
884
|
+
params.asset_types = options.types;
|
|
885
|
+
}
|
|
886
|
+
if (options.chains) {
|
|
887
|
+
params.chains = options.chains;
|
|
888
|
+
}
|
|
889
|
+
const result = await client.get(
|
|
890
|
+
"/api/v2/search",
|
|
891
|
+
params
|
|
892
|
+
);
|
|
893
|
+
console.log(formatOutput(result, getFormat2()));
|
|
872
894
|
}
|
|
873
895
|
);
|
|
874
|
-
cmd.command("accounts").description("Search accounts by username or address").argument("<query>", "Search query").option("--limit <limit>", "Number of results", "10").action(async (query, options) => {
|
|
875
|
-
const client = getClient2();
|
|
876
|
-
const result = await client.graphql(SEARCH_ACCOUNTS_QUERY, {
|
|
877
|
-
query,
|
|
878
|
-
limit: parseIntOption(options.limit, "--limit")
|
|
879
|
-
});
|
|
880
|
-
console.log(formatOutput(result.accountsByQuery, getFormat2()));
|
|
881
|
-
});
|
|
882
896
|
return cmd;
|
|
883
897
|
}
|
|
884
898
|
|
|
885
899
|
// src/commands/swaps.ts
|
|
886
|
-
import { Command as
|
|
900
|
+
import { Command as Command9 } from "commander";
|
|
887
901
|
function swapsCommand(getClient2, getFormat2) {
|
|
888
|
-
const cmd = new
|
|
902
|
+
const cmd = new Command9("swaps").description(
|
|
889
903
|
"Get swap quotes for token trading"
|
|
890
904
|
);
|
|
891
905
|
cmd.command("quote").description(
|
|
@@ -925,9 +939,9 @@ function swapsCommand(getClient2, getFormat2) {
|
|
|
925
939
|
}
|
|
926
940
|
|
|
927
941
|
// src/commands/tokens.ts
|
|
928
|
-
import { Command as
|
|
942
|
+
import { Command as Command10 } from "commander";
|
|
929
943
|
function tokensCommand(getClient2, getFormat2) {
|
|
930
|
-
const cmd = new
|
|
944
|
+
const cmd = new Command10("tokens").description(
|
|
931
945
|
"Query trending tokens, top tokens, and token details"
|
|
932
946
|
);
|
|
933
947
|
cmd.command("trending").description("Get trending tokens based on OpenSea's trending score").option("--chains <chains>", "Comma-separated list of chains to filter by").option("--limit <limit>", "Number of results (max 100)", "20").option("--next <cursor>", "Pagination cursor").action(
|
|
@@ -971,6 +985,9 @@ function tokensCommand(getClient2, getFormat2) {
|
|
|
971
985
|
}
|
|
972
986
|
|
|
973
987
|
// src/cli.ts
|
|
988
|
+
var EXIT_API_ERROR = 1;
|
|
989
|
+
var EXIT_AUTH_ERROR = 2;
|
|
990
|
+
var EXIT_RATE_LIMITED = 3;
|
|
974
991
|
var BANNER = `
|
|
975
992
|
____ _____
|
|
976
993
|
/ __ \\ / ____|
|
|
@@ -981,8 +998,11 @@ var BANNER = `
|
|
|
981
998
|
| |
|
|
982
999
|
|_|
|
|
983
1000
|
`;
|
|
984
|
-
var program = new
|
|
985
|
-
program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version(process.env.npm_package_version ?? "0.0.0").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr")
|
|
1001
|
+
var program = new Command11();
|
|
1002
|
+
program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version(process.env.npm_package_version ?? "0.0.0").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr").option(
|
|
1003
|
+
"--fields <fields>",
|
|
1004
|
+
"Comma-separated list of fields to include in output"
|
|
1005
|
+
).option("--max-lines <lines>", "Truncate output after N lines").option("--max-retries <n>", "Max retries on 429/5xx (0 to disable)", "3").option("--no-retry", "Disable request retries");
|
|
986
1006
|
function getClient() {
|
|
987
1007
|
const opts = program.opts();
|
|
988
1008
|
const apiKey = opts.apiKey ?? process.env.OPENSEA_API_KEY;
|
|
@@ -990,14 +1010,16 @@ function getClient() {
|
|
|
990
1010
|
console.error(
|
|
991
1011
|
"Error: API key required. Use --api-key or set OPENSEA_API_KEY environment variable."
|
|
992
1012
|
);
|
|
993
|
-
process.exit(
|
|
1013
|
+
process.exit(EXIT_AUTH_ERROR);
|
|
994
1014
|
}
|
|
1015
|
+
const maxRetries = opts.retry ? parseIntOption(opts.maxRetries, "--max-retries") : 0;
|
|
995
1016
|
return new OpenSeaClient({
|
|
996
1017
|
apiKey,
|
|
997
1018
|
chain: opts.chain,
|
|
998
1019
|
baseUrl: opts.baseUrl,
|
|
999
1020
|
timeout: parseIntOption(opts.timeout, "--timeout"),
|
|
1000
|
-
verbose: opts.verbose
|
|
1021
|
+
verbose: opts.verbose,
|
|
1022
|
+
maxRetries
|
|
1001
1023
|
});
|
|
1002
1024
|
}
|
|
1003
1025
|
function getFormat() {
|
|
@@ -1006,6 +1028,21 @@ function getFormat() {
|
|
|
1006
1028
|
if (opts.format === "toon") return "toon";
|
|
1007
1029
|
return "json";
|
|
1008
1030
|
}
|
|
1031
|
+
program.hook("preAction", () => {
|
|
1032
|
+
const opts = program.opts();
|
|
1033
|
+
let maxLines;
|
|
1034
|
+
if (opts.maxLines) {
|
|
1035
|
+
maxLines = parseIntOption(opts.maxLines, "--max-lines");
|
|
1036
|
+
if (maxLines < 1) {
|
|
1037
|
+
console.error("Error: --max-lines must be >= 1");
|
|
1038
|
+
process.exit(2);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
setOutputOptions({
|
|
1042
|
+
fields: opts.fields?.split(",").map((f) => f.trim()),
|
|
1043
|
+
maxLines
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1009
1046
|
program.addCommand(collectionsCommand(getClient, getFormat));
|
|
1010
1047
|
program.addCommand(nftsCommand(getClient, getFormat));
|
|
1011
1048
|
program.addCommand(listingsCommand(getClient, getFormat));
|
|
@@ -1015,15 +1052,17 @@ program.addCommand(accountsCommand(getClient, getFormat));
|
|
|
1015
1052
|
program.addCommand(tokensCommand(getClient, getFormat));
|
|
1016
1053
|
program.addCommand(searchCommand(getClient, getFormat));
|
|
1017
1054
|
program.addCommand(swapsCommand(getClient, getFormat));
|
|
1055
|
+
program.addCommand(healthCommand(getClient, getFormat));
|
|
1018
1056
|
async function main() {
|
|
1019
1057
|
try {
|
|
1020
1058
|
await program.parseAsync(process.argv);
|
|
1021
1059
|
} catch (error) {
|
|
1022
1060
|
if (error instanceof OpenSeaAPIError) {
|
|
1061
|
+
const isRateLimited = error.statusCode === 429;
|
|
1023
1062
|
console.error(
|
|
1024
1063
|
JSON.stringify(
|
|
1025
1064
|
{
|
|
1026
|
-
error: "API Error",
|
|
1065
|
+
error: isRateLimited ? "Rate Limited" : "API Error",
|
|
1027
1066
|
status: error.statusCode,
|
|
1028
1067
|
path: error.path,
|
|
1029
1068
|
message: error.responseBody
|
|
@@ -1032,7 +1071,7 @@ async function main() {
|
|
|
1032
1071
|
2
|
|
1033
1072
|
)
|
|
1034
1073
|
);
|
|
1035
|
-
process.exit(
|
|
1074
|
+
process.exit(isRateLimited ? EXIT_RATE_LIMITED : EXIT_API_ERROR);
|
|
1036
1075
|
}
|
|
1037
1076
|
const label = error instanceof TypeError ? "Network Error" : error.name;
|
|
1038
1077
|
console.error(
|
|
@@ -1045,7 +1084,7 @@ async function main() {
|
|
|
1045
1084
|
2
|
|
1046
1085
|
)
|
|
1047
1086
|
);
|
|
1048
|
-
process.exit(
|
|
1087
|
+
process.exit(EXIT_API_ERROR);
|
|
1049
1088
|
}
|
|
1050
1089
|
}
|
|
1051
1090
|
main();
|