@opensea/cli 0.4.1 → 1.0.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 +12 -1
- package/dist/cli.js +1484 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +508 -183
- package/dist/index.js +1276 -36
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/client.ts
|
|
7
7
|
var DEFAULT_BASE_URL = "https://api.opensea.io";
|
|
8
8
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
9
|
+
var USER_AGENT = `opensea-cli/${"1.0.0"}`;
|
|
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
|
+
}
|
|
9
27
|
var OpenSeaClient = class {
|
|
10
28
|
apiKey;
|
|
11
29
|
baseUrl;
|
|
12
30
|
defaultChain;
|
|
13
31
|
timeoutMs;
|
|
14
32
|
verbose;
|
|
33
|
+
maxRetries;
|
|
34
|
+
retryBaseDelay;
|
|
15
35
|
constructor(config) {
|
|
16
36
|
this.apiKey = config.apiKey;
|
|
17
37
|
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
18
38
|
this.defaultChain = config.chain ?? "ethereum";
|
|
19
39
|
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
20
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
|
+
};
|
|
21
50
|
}
|
|
22
51
|
async get(path, params) {
|
|
23
52
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
@@ -31,21 +60,14 @@ var OpenSeaClient = class {
|
|
|
31
60
|
if (this.verbose) {
|
|
32
61
|
console.error(`[verbose] GET ${url.toString()}`);
|
|
33
62
|
}
|
|
34
|
-
const response = await
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
const response = await this.fetchWithRetry(
|
|
64
|
+
url.toString(),
|
|
65
|
+
{
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: this.defaultHeaders
|
|
39
68
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (this.verbose) {
|
|
43
|
-
console.error(`[verbose] ${response.status} ${response.statusText}`);
|
|
44
|
-
}
|
|
45
|
-
if (!response.ok) {
|
|
46
|
-
const body = await response.text();
|
|
47
|
-
throw new OpenSeaAPIError(response.status, body, path);
|
|
48
|
-
}
|
|
69
|
+
path
|
|
70
|
+
);
|
|
49
71
|
return response.json();
|
|
50
72
|
}
|
|
51
73
|
async post(path, body, params) {
|
|
@@ -57,34 +79,67 @@ var OpenSeaClient = class {
|
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
|
-
const headers = {
|
|
61
|
-
Accept: "application/json",
|
|
62
|
-
"x-api-key": this.apiKey
|
|
63
|
-
};
|
|
82
|
+
const headers = { ...this.defaultHeaders };
|
|
64
83
|
if (body) {
|
|
65
84
|
headers["Content-Type"] = "application/json";
|
|
66
85
|
}
|
|
67
86
|
if (this.verbose) {
|
|
68
87
|
console.error(`[verbose] POST ${url.toString()}`);
|
|
69
88
|
}
|
|
70
|
-
const response = await
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
const text = await response.text();
|
|
81
|
-
throw new OpenSeaAPIError(response.status, text, path);
|
|
82
|
-
}
|
|
89
|
+
const response = await this.fetchWithRetry(
|
|
90
|
+
url.toString(),
|
|
91
|
+
{
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers,
|
|
94
|
+
body: body ? JSON.stringify(body) : void 0
|
|
95
|
+
},
|
|
96
|
+
path
|
|
97
|
+
);
|
|
83
98
|
return response.json();
|
|
84
99
|
}
|
|
85
100
|
getDefaultChain() {
|
|
86
101
|
return this.defaultChain;
|
|
87
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
|
+
}
|
|
88
143
|
};
|
|
89
144
|
var OpenSeaAPIError = class extends Error {
|
|
90
145
|
constructor(statusCode, responseBody, path) {
|
|
@@ -94,6 +149,9 @@ var OpenSeaAPIError = class extends Error {
|
|
|
94
149
|
this.path = path;
|
|
95
150
|
this.name = "OpenSeaAPIError";
|
|
96
151
|
}
|
|
152
|
+
statusCode;
|
|
153
|
+
responseBody;
|
|
154
|
+
path;
|
|
97
155
|
};
|
|
98
156
|
|
|
99
157
|
// src/commands/accounts.ts
|
|
@@ -370,14 +428,24 @@ function formatToon(data) {
|
|
|
370
428
|
}
|
|
371
429
|
|
|
372
430
|
// src/output.ts
|
|
431
|
+
var _outputOptions = {};
|
|
432
|
+
function setOutputOptions(options) {
|
|
433
|
+
_outputOptions = options;
|
|
434
|
+
}
|
|
373
435
|
function formatOutput(data, format) {
|
|
436
|
+
const processed = _outputOptions.fields ? filterFields(data, _outputOptions.fields) : data;
|
|
437
|
+
let result;
|
|
374
438
|
if (format === "table") {
|
|
375
|
-
|
|
439
|
+
result = formatTable(processed);
|
|
440
|
+
} else if (format === "toon") {
|
|
441
|
+
result = formatToon(processed);
|
|
442
|
+
} else {
|
|
443
|
+
result = JSON.stringify(processed, null, 2);
|
|
376
444
|
}
|
|
377
|
-
if (
|
|
378
|
-
|
|
445
|
+
if (_outputOptions.maxLines != null) {
|
|
446
|
+
result = truncateOutput(result, _outputOptions.maxLines);
|
|
379
447
|
}
|
|
380
|
-
return
|
|
448
|
+
return result;
|
|
381
449
|
}
|
|
382
450
|
function formatTable(data) {
|
|
383
451
|
if (Array.isArray(data)) {
|
|
@@ -413,20 +481,31 @@ function formatTable(data) {
|
|
|
413
481
|
}
|
|
414
482
|
return String(data);
|
|
415
483
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
484
|
+
function pickFields(obj, fields) {
|
|
485
|
+
const result = {};
|
|
486
|
+
for (const field of fields) {
|
|
487
|
+
if (field in obj) {
|
|
488
|
+
result[field] = obj[field];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
function filterFields(data, fields) {
|
|
494
|
+
if (Array.isArray(data)) {
|
|
495
|
+
return data.map((item) => filterFields(item, fields));
|
|
496
|
+
}
|
|
497
|
+
if (data && typeof data === "object") {
|
|
498
|
+
return pickFields(data, fields);
|
|
499
|
+
}
|
|
500
|
+
return data;
|
|
501
|
+
}
|
|
502
|
+
function truncateOutput(text, maxLines) {
|
|
503
|
+
const lines = text.split("\n");
|
|
504
|
+
if (lines.length <= maxLines) return text;
|
|
505
|
+
const omitted = lines.length - maxLines;
|
|
506
|
+
return lines.slice(0, maxLines).join("\n") + `
|
|
507
|
+
... (${omitted} more line${omitted === 1 ? "" : "s"})`;
|
|
426
508
|
}
|
|
427
|
-
|
|
428
|
-
// src/commands/collections.ts
|
|
429
|
-
import { Command as Command2 } from "commander";
|
|
430
509
|
|
|
431
510
|
// src/parse.ts
|
|
432
511
|
function parseIntOption(value, name) {
|
|
@@ -444,9 +523,65 @@ function parseFloatOption(value, name) {
|
|
|
444
523
|
return parsed;
|
|
445
524
|
}
|
|
446
525
|
|
|
526
|
+
// src/commands/accounts.ts
|
|
527
|
+
function accountsCommand(getClient2, getFormat2) {
|
|
528
|
+
const cmd = new Command("accounts").description("Query accounts");
|
|
529
|
+
cmd.command("get").description("Get an account by address").argument("<address>", "Wallet address").action(async (address) => {
|
|
530
|
+
const client = getClient2();
|
|
531
|
+
const result = await client.get(`/api/v2/accounts/${address}`);
|
|
532
|
+
console.log(formatOutput(result, getFormat2()));
|
|
533
|
+
});
|
|
534
|
+
cmd.command("tokens").description("Get token balances for an account").argument("<address>", "Wallet address").option("--chains <chains>", "Comma-separated list of chains to filter by").option("--limit <limit>", "Number of results", "20").option(
|
|
535
|
+
"--sort-by <field>",
|
|
536
|
+
"Sort by field (USD_VALUE, MARKET_CAP, ONE_DAY_VOLUME, PRICE, ONE_DAY_PRICE_CHANGE, SEVEN_DAY_PRICE_CHANGE)"
|
|
537
|
+
).option("--sort-direction <dir>", "Sort direction (asc, desc)").option("--next <cursor>", "Pagination cursor").option("--no-spam-filter", "Disable spam token filtering").action(
|
|
538
|
+
async (address, options) => {
|
|
539
|
+
const client = getClient2();
|
|
540
|
+
const result = await client.get(
|
|
541
|
+
`/api/v2/account/${address}/tokens`,
|
|
542
|
+
{
|
|
543
|
+
chains: options.chains,
|
|
544
|
+
limit: parseIntOption(options.limit, "--limit"),
|
|
545
|
+
sort_by: options.sortBy,
|
|
546
|
+
sort_direction: options.sortDirection,
|
|
547
|
+
cursor: options.next,
|
|
548
|
+
disable_spam_filtering: options.spamFilter ? void 0 : true
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
console.log(formatOutput(result, getFormat2()));
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
cmd.command("resolve").description(
|
|
555
|
+
"Resolve an ENS name, OpenSea username, or wallet address to canonical account info"
|
|
556
|
+
).argument(
|
|
557
|
+
"<identifier>",
|
|
558
|
+
"ENS name (e.g. vitalik.eth), OpenSea username, or wallet address"
|
|
559
|
+
).action(async (identifier) => {
|
|
560
|
+
const client = getClient2();
|
|
561
|
+
const result = await client.get(
|
|
562
|
+
`/api/v2/accounts/resolve/${identifier}`
|
|
563
|
+
);
|
|
564
|
+
console.log(formatOutput(result, getFormat2()));
|
|
565
|
+
});
|
|
566
|
+
return cmd;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/commands/chains.ts
|
|
570
|
+
import { Command as Command2 } from "commander";
|
|
571
|
+
function chainsCommand(getClient2, getFormat2) {
|
|
572
|
+
const cmd = new Command2("chains").description("Query supported blockchains");
|
|
573
|
+
cmd.command("list").description("List all supported blockchains and their capabilities").action(async () => {
|
|
574
|
+
const client = getClient2();
|
|
575
|
+
const result = await client.get("/api/v2/chains");
|
|
576
|
+
console.log(formatOutput(result, getFormat2()));
|
|
577
|
+
});
|
|
578
|
+
return cmd;
|
|
579
|
+
}
|
|
580
|
+
|
|
447
581
|
// src/commands/collections.ts
|
|
582
|
+
import { Command as Command3 } from "commander";
|
|
448
583
|
function collectionsCommand(getClient2, getFormat2) {
|
|
449
|
-
const cmd = new
|
|
584
|
+
const cmd = new Command3("collections").description(
|
|
450
585
|
"Manage and query NFT collections"
|
|
451
586
|
);
|
|
452
587
|
cmd.command("get").description("Get a single collection by slug").argument("<slug>", "Collection slug").action(async (slug) => {
|
|
@@ -485,13 +620,105 @@ function collectionsCommand(getClient2, getFormat2) {
|
|
|
485
620
|
);
|
|
486
621
|
console.log(formatOutput(result, getFormat2()));
|
|
487
622
|
});
|
|
623
|
+
cmd.command("trending").description("Get trending collections by sales activity").option(
|
|
624
|
+
"--timeframe <timeframe>",
|
|
625
|
+
"Time window (one_minute, five_minutes, fifteen_minutes, one_hour, one_day, seven_days, thirty_days, one_year, all_time)",
|
|
626
|
+
"one_day"
|
|
627
|
+
).option("--chains <chains>", "Comma-separated list of chains to filter by").option(
|
|
628
|
+
"--category <category>",
|
|
629
|
+
"Category (art, gaming, memberships, music, pfps, photography, domain-names, virtual-worlds, sports-collectibles)"
|
|
630
|
+
).option("--limit <limit>", "Number of results (max 100)", "20").option("--next <cursor>", "Pagination cursor").action(
|
|
631
|
+
async (options) => {
|
|
632
|
+
const client = getClient2();
|
|
633
|
+
const result = await client.get(
|
|
634
|
+
"/api/v2/collections/trending",
|
|
635
|
+
{
|
|
636
|
+
timeframe: options.timeframe,
|
|
637
|
+
chains: options.chains,
|
|
638
|
+
category: options.category,
|
|
639
|
+
limit: parseIntOption(options.limit, "--limit"),
|
|
640
|
+
cursor: options.next
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
console.log(formatOutput(result, getFormat2()));
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
cmd.command("top").description("Get top collections ranked by volume, sales, or floor price").option(
|
|
647
|
+
"--sort-by <field>",
|
|
648
|
+
"Sort by (one_day_volume, seven_days_volume, thirty_days_volume, floor_price, one_day_sales, seven_days_sales, thirty_days_sales, total_volume, total_sales)",
|
|
649
|
+
"one_day_volume"
|
|
650
|
+
).option("--chains <chains>", "Comma-separated list of chains to filter by").option(
|
|
651
|
+
"--category <category>",
|
|
652
|
+
"Category (art, gaming, memberships, music, pfps, photography, domain-names, virtual-worlds, sports-collectibles)"
|
|
653
|
+
).option("--limit <limit>", "Number of results (max 100)", "50").option("--next <cursor>", "Pagination cursor").action(
|
|
654
|
+
async (options) => {
|
|
655
|
+
const client = getClient2();
|
|
656
|
+
const result = await client.get(
|
|
657
|
+
"/api/v2/collections/top",
|
|
658
|
+
{
|
|
659
|
+
sort_by: options.sortBy,
|
|
660
|
+
chains: options.chains,
|
|
661
|
+
category: options.category,
|
|
662
|
+
limit: parseIntOption(options.limit, "--limit"),
|
|
663
|
+
cursor: options.next
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
console.log(formatOutput(result, getFormat2()));
|
|
667
|
+
}
|
|
668
|
+
);
|
|
669
|
+
return cmd;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/commands/drops.ts
|
|
673
|
+
import { Command as Command4 } from "commander";
|
|
674
|
+
function dropsCommand(getClient2, getFormat2) {
|
|
675
|
+
const cmd = new Command4("drops").description("Query and mint NFT drops");
|
|
676
|
+
cmd.command("list").description("List drops (featured, upcoming, or recently minted)").option(
|
|
677
|
+
"--type <type>",
|
|
678
|
+
"Drop type: featured, upcoming, or recently_minted",
|
|
679
|
+
"featured"
|
|
680
|
+
).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(
|
|
681
|
+
async (options) => {
|
|
682
|
+
const client = getClient2();
|
|
683
|
+
const result = await client.get(
|
|
684
|
+
"/api/v2/drops",
|
|
685
|
+
{
|
|
686
|
+
type: options.type,
|
|
687
|
+
chains: options.chains,
|
|
688
|
+
limit: parseIntOption(options.limit, "--limit"),
|
|
689
|
+
cursor: options.next
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
console.log(formatOutput(result, getFormat2()));
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
cmd.command("get").description("Get detailed drop info by collection slug").argument("<slug>", "Collection slug").action(async (slug) => {
|
|
696
|
+
const client = getClient2();
|
|
697
|
+
const result = await client.get(
|
|
698
|
+
`/api/v2/drops/${slug}`
|
|
699
|
+
);
|
|
700
|
+
console.log(formatOutput(result, getFormat2()));
|
|
701
|
+
});
|
|
702
|
+
cmd.command("mint").description("Build a mint transaction for a drop").argument("<slug>", "Collection slug").requiredOption("--minter <address>", "Wallet address to receive tokens").option("--quantity <n>", "Number of tokens to mint", "1").action(
|
|
703
|
+
async (slug, options) => {
|
|
704
|
+
const client = getClient2();
|
|
705
|
+
const result = await client.post(
|
|
706
|
+
`/api/v2/drops/${slug}/mint`,
|
|
707
|
+
{
|
|
708
|
+
minter: options.minter,
|
|
709
|
+
quantity: parseIntOption(options.quantity, "--quantity")
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
console.log(formatOutput(result, getFormat2()));
|
|
713
|
+
}
|
|
714
|
+
);
|
|
488
715
|
return cmd;
|
|
489
716
|
}
|
|
490
717
|
|
|
491
718
|
// src/commands/events.ts
|
|
492
|
-
import { Command as
|
|
719
|
+
import { Command as Command5 } from "commander";
|
|
493
720
|
function eventsCommand(getClient2, getFormat2) {
|
|
494
|
-
const cmd = new
|
|
721
|
+
const cmd = new Command5("events").description("Query marketplace events");
|
|
495
722
|
cmd.command("list").description("List events").option(
|
|
496
723
|
"--event-type <type>",
|
|
497
724
|
"Event type (sale, transfer, mint, listing, offer, trait_offer, collection_offer)"
|
|
@@ -549,10 +776,88 @@ function eventsCommand(getClient2, getFormat2) {
|
|
|
549
776
|
return cmd;
|
|
550
777
|
}
|
|
551
778
|
|
|
779
|
+
// src/commands/health.ts
|
|
780
|
+
import { Command as Command6 } from "commander";
|
|
781
|
+
|
|
782
|
+
// src/health.ts
|
|
783
|
+
async function checkHealth(client) {
|
|
784
|
+
const keyPrefix = client.getApiKeyPrefix();
|
|
785
|
+
try {
|
|
786
|
+
await client.get("/api/v2/collections", { limit: 1 });
|
|
787
|
+
} catch (error) {
|
|
788
|
+
let message;
|
|
789
|
+
if (error instanceof OpenSeaAPIError) {
|
|
790
|
+
message = error.statusCode === 429 ? "Rate limited: too many requests" : `API error (${error.statusCode}): ${error.responseBody}`;
|
|
791
|
+
} else {
|
|
792
|
+
message = `Network error: ${error.message}`;
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
status: "error",
|
|
796
|
+
key_prefix: keyPrefix,
|
|
797
|
+
authenticated: false,
|
|
798
|
+
rate_limited: error instanceof OpenSeaAPIError && error.statusCode === 429,
|
|
799
|
+
message
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
await client.get("/api/v2/listings/collection/boredapeyachtclub/all", {
|
|
804
|
+
limit: 1
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
status: "ok",
|
|
808
|
+
key_prefix: keyPrefix,
|
|
809
|
+
authenticated: true,
|
|
810
|
+
rate_limited: false,
|
|
811
|
+
message: "Connectivity and authentication are working"
|
|
812
|
+
};
|
|
813
|
+
} catch (error) {
|
|
814
|
+
if (error instanceof OpenSeaAPIError) {
|
|
815
|
+
if (error.statusCode === 429) {
|
|
816
|
+
return {
|
|
817
|
+
status: "error",
|
|
818
|
+
key_prefix: keyPrefix,
|
|
819
|
+
authenticated: false,
|
|
820
|
+
rate_limited: true,
|
|
821
|
+
message: "Rate limited: too many requests"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (error.statusCode === 401 || error.statusCode === 403) {
|
|
825
|
+
return {
|
|
826
|
+
status: "error",
|
|
827
|
+
key_prefix: keyPrefix,
|
|
828
|
+
authenticated: false,
|
|
829
|
+
rate_limited: false,
|
|
830
|
+
message: `Authentication failed (${error.statusCode}): invalid API key`
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
status: "ok",
|
|
836
|
+
key_prefix: keyPrefix,
|
|
837
|
+
authenticated: false,
|
|
838
|
+
rate_limited: false,
|
|
839
|
+
message: "Connectivity is working but authentication could not be verified"
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/commands/health.ts
|
|
845
|
+
function healthCommand(getClient2, getFormat2) {
|
|
846
|
+
const cmd = new Command6("health").description("Check API connectivity and authentication").action(async () => {
|
|
847
|
+
const client = getClient2();
|
|
848
|
+
const result = await checkHealth(client);
|
|
849
|
+
console.log(formatOutput(result, getFormat2()));
|
|
850
|
+
if (result.status === "error") {
|
|
851
|
+
process.exit(result.rate_limited ? 3 : 1);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
return cmd;
|
|
855
|
+
}
|
|
856
|
+
|
|
552
857
|
// src/commands/listings.ts
|
|
553
|
-
import { Command as
|
|
858
|
+
import { Command as Command7 } from "commander";
|
|
554
859
|
function listingsCommand(getClient2, getFormat2) {
|
|
555
|
-
const cmd = new
|
|
860
|
+
const cmd = new Command7("listings").description("Query NFT listings");
|
|
556
861
|
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(
|
|
557
862
|
async (collection, options) => {
|
|
558
863
|
const client = getClient2();
|
|
@@ -584,9 +889,9 @@ function listingsCommand(getClient2, getFormat2) {
|
|
|
584
889
|
}
|
|
585
890
|
|
|
586
891
|
// src/commands/nfts.ts
|
|
587
|
-
import { Command as
|
|
892
|
+
import { Command as Command8 } from "commander";
|
|
588
893
|
function nftsCommand(getClient2, getFormat2) {
|
|
589
|
-
const cmd = new
|
|
894
|
+
const cmd = new Command8("nfts").description("Query NFTs");
|
|
590
895
|
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) => {
|
|
591
896
|
const client = getClient2();
|
|
592
897
|
const result = await client.get(
|
|
@@ -650,13 +955,31 @@ function nftsCommand(getClient2, getFormat2) {
|
|
|
650
955
|
);
|
|
651
956
|
console.log(formatOutput(result, getFormat2()));
|
|
652
957
|
});
|
|
958
|
+
cmd.command("validate-metadata").description("Validate NFT metadata by fetching and parsing it").argument("<chain>", "Chain").argument("<contract>", "Contract address").argument("<token-id>", "Token ID").option(
|
|
959
|
+
"--ignore-cache",
|
|
960
|
+
"Ignore cached item URLs and re-fetch from source"
|
|
961
|
+
).action(
|
|
962
|
+
async (chain, contract, tokenId, options) => {
|
|
963
|
+
const client = getClient2();
|
|
964
|
+
const params = {};
|
|
965
|
+
if (options.ignoreCache) {
|
|
966
|
+
params.ignoreCachedItemUrls = true;
|
|
967
|
+
}
|
|
968
|
+
const result = await client.post(
|
|
969
|
+
`/api/v2/chain/${chain}/contract/${contract}/nfts/${tokenId}/validate-metadata`,
|
|
970
|
+
void 0,
|
|
971
|
+
params
|
|
972
|
+
);
|
|
973
|
+
console.log(formatOutput(result, getFormat2()));
|
|
974
|
+
}
|
|
975
|
+
);
|
|
653
976
|
return cmd;
|
|
654
977
|
}
|
|
655
978
|
|
|
656
979
|
// src/commands/offers.ts
|
|
657
|
-
import { Command as
|
|
980
|
+
import { Command as Command9 } from "commander";
|
|
658
981
|
function offersCommand(getClient2, getFormat2) {
|
|
659
|
-
const cmd = new
|
|
982
|
+
const cmd = new Command9("offers").description("Query NFT offers");
|
|
660
983
|
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(
|
|
661
984
|
async (collection, options) => {
|
|
662
985
|
const client = getClient2();
|
|
@@ -700,9 +1023,9 @@ function offersCommand(getClient2, getFormat2) {
|
|
|
700
1023
|
}
|
|
701
1024
|
|
|
702
1025
|
// src/commands/search.ts
|
|
703
|
-
import { Command as
|
|
1026
|
+
import { Command as Command10 } from "commander";
|
|
704
1027
|
function searchCommand(getClient2, getFormat2) {
|
|
705
|
-
const cmd = new
|
|
1028
|
+
const cmd = new Command10("search").description("Search across collections, tokens, NFTs, and accounts").argument("<query>", "Search query").option(
|
|
706
1029
|
"--types <types>",
|
|
707
1030
|
"Filter by type (comma-separated: collection,nft,token,account)"
|
|
708
1031
|
).option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "20").action(
|
|
@@ -729,10 +1052,1012 @@ function searchCommand(getClient2, getFormat2) {
|
|
|
729
1052
|
}
|
|
730
1053
|
|
|
731
1054
|
// src/commands/swaps.ts
|
|
732
|
-
import { Command as
|
|
1055
|
+
import { Command as Command11 } from "commander";
|
|
1056
|
+
|
|
1057
|
+
// src/wallet/fireblocks.generated.ts
|
|
1058
|
+
var CHAIN_TO_FIREBLOCKS_ASSET = {
|
|
1059
|
+
1: "ETH",
|
|
1060
|
+
10: "ETH-OPT",
|
|
1061
|
+
130: "UNICHAIN_ETH",
|
|
1062
|
+
137: "MATIC_POLYGON",
|
|
1063
|
+
360: "SHAPE_ETH",
|
|
1064
|
+
1329: "SEI_EVM",
|
|
1065
|
+
1868: "SONEIUM_ETH",
|
|
1066
|
+
2741: "ABSTRACT_ETH",
|
|
1067
|
+
8453: "BASECHAIN_ETH",
|
|
1068
|
+
33139: "APE_CHAIN",
|
|
1069
|
+
42161: "ETH-AETH",
|
|
1070
|
+
43114: "AVAX",
|
|
1071
|
+
80094: "BERA_CHAIN",
|
|
1072
|
+
81457: "BLAST_ETH",
|
|
1073
|
+
7777777: "ZORA_ETH"
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// src/wallet/fireblocks.ts
|
|
1077
|
+
var FIREBLOCKS_API_BASE = "https://api.fireblocks.io";
|
|
1078
|
+
var FireblocksAdapter = class _FireblocksAdapter {
|
|
1079
|
+
name = "fireblocks";
|
|
1080
|
+
onRequest;
|
|
1081
|
+
onResponse;
|
|
1082
|
+
config;
|
|
1083
|
+
cachedAddress;
|
|
1084
|
+
constructor(config) {
|
|
1085
|
+
this.config = config;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Create a FireblocksAdapter from environment variables.
|
|
1089
|
+
* Throws if any required variable is missing.
|
|
1090
|
+
*/
|
|
1091
|
+
static fromEnv() {
|
|
1092
|
+
const apiKey = process.env.FIREBLOCKS_API_KEY;
|
|
1093
|
+
const apiSecret = process.env.FIREBLOCKS_API_SECRET;
|
|
1094
|
+
const vaultId = process.env.FIREBLOCKS_VAULT_ID;
|
|
1095
|
+
if (!apiKey) {
|
|
1096
|
+
throw new Error("FIREBLOCKS_API_KEY environment variable is required");
|
|
1097
|
+
}
|
|
1098
|
+
if (!apiSecret) {
|
|
1099
|
+
throw new Error("FIREBLOCKS_API_SECRET environment variable is required");
|
|
1100
|
+
}
|
|
1101
|
+
if (!vaultId) {
|
|
1102
|
+
throw new Error("FIREBLOCKS_VAULT_ID environment variable is required");
|
|
1103
|
+
}
|
|
1104
|
+
return new _FireblocksAdapter({
|
|
1105
|
+
apiKey,
|
|
1106
|
+
apiSecret,
|
|
1107
|
+
vaultId,
|
|
1108
|
+
assetId: process.env.FIREBLOCKS_ASSET_ID,
|
|
1109
|
+
baseUrl: process.env.FIREBLOCKS_API_BASE_URL
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
get baseUrl() {
|
|
1113
|
+
return this.config.baseUrl ?? FIREBLOCKS_API_BASE;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Create a JWT for Fireblocks API authentication.
|
|
1117
|
+
*
|
|
1118
|
+
* Fireblocks uses JWT tokens signed with the API secret (RSA private key).
|
|
1119
|
+
* The JWT contains the API key as `sub`, a URI claim for the endpoint path,
|
|
1120
|
+
* and a body hash for POST requests.
|
|
1121
|
+
*
|
|
1122
|
+
* @see https://developers.fireblocks.com/reference/signing-a-request-jwt-structure
|
|
1123
|
+
*/
|
|
1124
|
+
async createJwt(path, bodyHash) {
|
|
1125
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1126
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
1127
|
+
const payload = {
|
|
1128
|
+
uri: path,
|
|
1129
|
+
nonce: crypto.randomUUID(),
|
|
1130
|
+
iat: now,
|
|
1131
|
+
exp: now + 30,
|
|
1132
|
+
sub: this.config.apiKey,
|
|
1133
|
+
bodyHash
|
|
1134
|
+
};
|
|
1135
|
+
const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
|
|
1136
|
+
const unsigned = `${b64url(header)}.${b64url(payload)}`;
|
|
1137
|
+
const key = await crypto.subtle.importKey(
|
|
1138
|
+
"pkcs8",
|
|
1139
|
+
this.pemToBuffer(this.config.apiSecret),
|
|
1140
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1141
|
+
false,
|
|
1142
|
+
["sign"]
|
|
1143
|
+
);
|
|
1144
|
+
const sig = await crypto.subtle.sign(
|
|
1145
|
+
"RSASSA-PKCS1-v1_5",
|
|
1146
|
+
key,
|
|
1147
|
+
new TextEncoder().encode(unsigned)
|
|
1148
|
+
);
|
|
1149
|
+
return `${unsigned}.${Buffer.from(sig).toString("base64url")}`;
|
|
1150
|
+
}
|
|
1151
|
+
pemToBuffer(pem) {
|
|
1152
|
+
const lines = pem.replace(/-----BEGIN .*-----/, "").replace(/-----END .*-----/, "").replace(/\s/g, "");
|
|
1153
|
+
const buf = Buffer.from(lines, "base64");
|
|
1154
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
1155
|
+
}
|
|
1156
|
+
async hashBody(body) {
|
|
1157
|
+
const hash = await crypto.subtle.digest(
|
|
1158
|
+
"SHA-256",
|
|
1159
|
+
new TextEncoder().encode(body)
|
|
1160
|
+
);
|
|
1161
|
+
return Buffer.from(hash).toString("hex");
|
|
1162
|
+
}
|
|
1163
|
+
resolveAssetId(chainId) {
|
|
1164
|
+
if (this.config.assetId) return this.config.assetId;
|
|
1165
|
+
const asset = CHAIN_TO_FIREBLOCKS_ASSET[chainId];
|
|
1166
|
+
if (!asset) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`No Fireblocks asset ID mapping for chain ${chainId}. Set FIREBLOCKS_ASSET_ID explicitly or use a supported chain: ${Object.keys(CHAIN_TO_FIREBLOCKS_ASSET).join(", ")}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return asset;
|
|
1172
|
+
}
|
|
1173
|
+
async getAddress() {
|
|
1174
|
+
if (this.cachedAddress) return this.cachedAddress;
|
|
1175
|
+
const assetId = this.config.assetId ?? "ETH";
|
|
1176
|
+
const path = `/v1/vault/accounts/${this.config.vaultId}/${assetId}/addresses`;
|
|
1177
|
+
const bodyHash = await this.hashBody("");
|
|
1178
|
+
const jwt = await this.createJwt(path, bodyHash);
|
|
1179
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1180
|
+
headers: {
|
|
1181
|
+
"X-API-Key": this.config.apiKey,
|
|
1182
|
+
Authorization: `Bearer ${jwt}`
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
if (!response.ok) {
|
|
1186
|
+
const body = await response.text();
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Fireblocks getAddress failed (${response.status}): ${body}`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const data = await response.json();
|
|
1192
|
+
if (!data[0]?.address) {
|
|
1193
|
+
throw new Error("Fireblocks returned no addresses for vault");
|
|
1194
|
+
}
|
|
1195
|
+
this.cachedAddress = data[0].address;
|
|
1196
|
+
return data[0].address;
|
|
1197
|
+
}
|
|
1198
|
+
async sendTransaction(tx) {
|
|
1199
|
+
this.onRequest?.("sendTransaction", tx);
|
|
1200
|
+
const startTime = Date.now();
|
|
1201
|
+
const assetId = this.resolveAssetId(tx.chainId);
|
|
1202
|
+
const path = "/v1/transactions";
|
|
1203
|
+
const requestBody = {
|
|
1204
|
+
assetId,
|
|
1205
|
+
operation: "CONTRACT_CALL",
|
|
1206
|
+
source: {
|
|
1207
|
+
type: "VAULT_ACCOUNT",
|
|
1208
|
+
id: this.config.vaultId
|
|
1209
|
+
},
|
|
1210
|
+
destination: {
|
|
1211
|
+
type: "ONE_TIME_ADDRESS",
|
|
1212
|
+
oneTimeAddress: { address: tx.to }
|
|
1213
|
+
},
|
|
1214
|
+
amount: tx.value === "0" ? "0" : tx.value,
|
|
1215
|
+
extraParameters: {
|
|
1216
|
+
contractCallData: tx.data
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
const bodyStr = JSON.stringify(requestBody);
|
|
1220
|
+
const bodyHash = await this.hashBody(bodyStr);
|
|
1221
|
+
const jwt = await this.createJwt(path, bodyHash);
|
|
1222
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: {
|
|
1225
|
+
"Content-Type": "application/json",
|
|
1226
|
+
"X-API-Key": this.config.apiKey,
|
|
1227
|
+
Authorization: `Bearer ${jwt}`
|
|
1228
|
+
},
|
|
1229
|
+
body: bodyStr
|
|
1230
|
+
});
|
|
1231
|
+
if (!response.ok) {
|
|
1232
|
+
const body = await response.text();
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`Fireblocks sendTransaction failed (${response.status}): ${body}`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const data = await response.json();
|
|
1238
|
+
if (data.txHash) {
|
|
1239
|
+
const result2 = { hash: data.txHash };
|
|
1240
|
+
this.onResponse?.("sendTransaction", result2, Date.now() - startTime);
|
|
1241
|
+
return result2;
|
|
1242
|
+
}
|
|
1243
|
+
const result = await this.waitForTransaction(data.id);
|
|
1244
|
+
this.onResponse?.("sendTransaction", result, Date.now() - startTime);
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Poll a Fireblocks transaction until it reaches a terminal status.
|
|
1249
|
+
* Fireblocks MPC signing + broadcast is asynchronous, so the initial
|
|
1250
|
+
* POST returns a transaction ID that must be polled for the final hash.
|
|
1251
|
+
*/
|
|
1252
|
+
async waitForTransaction(txId) {
|
|
1253
|
+
const maxAttempts = process.env.FIREBLOCKS_MAX_POLL_ATTEMPTS ? Number.parseInt(process.env.FIREBLOCKS_MAX_POLL_ATTEMPTS, 10) : 60;
|
|
1254
|
+
const pollIntervalMs = 2e3;
|
|
1255
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1256
|
+
const path = `/v1/transactions/${txId}`;
|
|
1257
|
+
const bodyHash = await this.hashBody("");
|
|
1258
|
+
const jwt = await this.createJwt(path, bodyHash);
|
|
1259
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1260
|
+
headers: {
|
|
1261
|
+
"X-API-Key": this.config.apiKey,
|
|
1262
|
+
Authorization: `Bearer ${jwt}`
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
if (!response.ok) {
|
|
1266
|
+
const body = await response.text();
|
|
1267
|
+
throw new Error(`Fireblocks poll failed (${response.status}): ${body}`);
|
|
1268
|
+
}
|
|
1269
|
+
const data = await response.json();
|
|
1270
|
+
if (data.status === "COMPLETED" && data.txHash) {
|
|
1271
|
+
return { hash: data.txHash };
|
|
1272
|
+
}
|
|
1273
|
+
if (data.status === "FAILED" || data.status === "REJECTED" || data.status === "CANCELLED" || data.status === "BLOCKED") {
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
`Fireblocks transaction ${txId} ended with status: ${data.status}`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1279
|
+
}
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`Fireblocks transaction ${txId} did not complete within ${maxAttempts * pollIntervalMs / 1e3}s`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
// src/wallet/private-key.ts
|
|
1287
|
+
var HOSTED_RPC_PROVIDERS = [
|
|
1288
|
+
"infura.io",
|
|
1289
|
+
"alchemy.com",
|
|
1290
|
+
"quicknode.com",
|
|
1291
|
+
"ankr.com",
|
|
1292
|
+
"cloudflare-eth.com",
|
|
1293
|
+
"pokt.network",
|
|
1294
|
+
"blastapi.io",
|
|
1295
|
+
"chainnodes.org",
|
|
1296
|
+
"drpc.org"
|
|
1297
|
+
];
|
|
1298
|
+
var PrivateKeyAdapter = class _PrivateKeyAdapter {
|
|
1299
|
+
name = "private-key";
|
|
1300
|
+
onRequest;
|
|
1301
|
+
onResponse;
|
|
1302
|
+
config;
|
|
1303
|
+
hasWarned = false;
|
|
1304
|
+
constructor(config) {
|
|
1305
|
+
this.config = config;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Create a PrivateKeyAdapter from environment variables.
|
|
1309
|
+
* Validates the private key format and warns if the RPC URL looks
|
|
1310
|
+
* like a hosted provider (which won't support eth_sendTransaction).
|
|
1311
|
+
*/
|
|
1312
|
+
static fromEnv() {
|
|
1313
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
1314
|
+
const rpcUrl = process.env.RPC_URL;
|
|
1315
|
+
const walletAddress = process.env.WALLET_ADDRESS;
|
|
1316
|
+
if (!privateKey) {
|
|
1317
|
+
throw new Error("PRIVATE_KEY environment variable is required");
|
|
1318
|
+
}
|
|
1319
|
+
if (!rpcUrl) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
"RPC_URL environment variable is required when using PRIVATE_KEY"
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
if (!walletAddress) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
"WALLET_ADDRESS environment variable is required when using PRIVATE_KEY"
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const cleanKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
|
|
1330
|
+
if (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
"PRIVATE_KEY must be a 32-byte hex string (64 hex characters, with optional 0x prefix)"
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
const host = new URL(rpcUrl).hostname;
|
|
1337
|
+
const isHosted = HOSTED_RPC_PROVIDERS.some(
|
|
1338
|
+
(provider) => host.includes(provider)
|
|
1339
|
+
);
|
|
1340
|
+
if (isHosted) {
|
|
1341
|
+
console.warn(
|
|
1342
|
+
`WARNING: RPC_URL (${host}) looks like a hosted provider. The private-key adapter uses eth_sendTransaction which only works with local dev nodes (Hardhat, Anvil, Ganache). Hosted providers will reject this call.`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
return new _PrivateKeyAdapter({ privateKey, rpcUrl, walletAddress });
|
|
1348
|
+
}
|
|
1349
|
+
async getAddress() {
|
|
1350
|
+
return this.config.walletAddress;
|
|
1351
|
+
}
|
|
1352
|
+
async sendTransaction(tx) {
|
|
1353
|
+
if (!this.hasWarned) {
|
|
1354
|
+
this.hasWarned = true;
|
|
1355
|
+
console.warn(
|
|
1356
|
+
"WARNING: Using raw PRIVATE_KEY adapter. This is not recommended for production. Use --wallet-provider privy|turnkey|fireblocks for managed wallet security."
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
this.onRequest?.("sendTransaction", tx);
|
|
1360
|
+
const startTime = Date.now();
|
|
1361
|
+
const response = await fetch(this.config.rpcUrl, {
|
|
1362
|
+
method: "POST",
|
|
1363
|
+
headers: { "Content-Type": "application/json" },
|
|
1364
|
+
body: JSON.stringify({
|
|
1365
|
+
jsonrpc: "2.0",
|
|
1366
|
+
id: 1,
|
|
1367
|
+
method: "eth_sendTransaction",
|
|
1368
|
+
params: [
|
|
1369
|
+
{
|
|
1370
|
+
from: this.config.walletAddress,
|
|
1371
|
+
to: tx.to,
|
|
1372
|
+
data: tx.data,
|
|
1373
|
+
value: tx.value === "0" ? "0x0" : `0x${BigInt(tx.value).toString(16)}`,
|
|
1374
|
+
chainId: `0x${tx.chainId.toString(16)}`
|
|
1375
|
+
}
|
|
1376
|
+
]
|
|
1377
|
+
})
|
|
1378
|
+
});
|
|
1379
|
+
if (!response.ok) {
|
|
1380
|
+
const body = await response.text();
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`Private key sendTransaction failed (${response.status}): ${body}`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const data = await response.json();
|
|
1386
|
+
if (data.error) {
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
`Private key sendTransaction RPC error: ${data.error.message}`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
if (!data.result) {
|
|
1392
|
+
throw new Error("Private key sendTransaction returned no tx hash");
|
|
1393
|
+
}
|
|
1394
|
+
const result = { hash: data.result };
|
|
1395
|
+
this.onResponse?.("sendTransaction", result, Date.now() - startTime);
|
|
1396
|
+
return result;
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
// src/wallet/privy.ts
|
|
1401
|
+
var PRIVY_API_BASE = "https://api.privy.io";
|
|
1402
|
+
var PrivyAdapter = class _PrivyAdapter {
|
|
1403
|
+
name = "privy";
|
|
1404
|
+
onRequest;
|
|
1405
|
+
onResponse;
|
|
1406
|
+
config;
|
|
1407
|
+
cachedAddress;
|
|
1408
|
+
constructor(config) {
|
|
1409
|
+
this.config = config;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Create a PrivyAdapter from environment variables.
|
|
1413
|
+
* Throws if any required variable is missing.
|
|
1414
|
+
*/
|
|
1415
|
+
static fromEnv() {
|
|
1416
|
+
const appId = process.env.PRIVY_APP_ID;
|
|
1417
|
+
const appSecret = process.env.PRIVY_APP_SECRET;
|
|
1418
|
+
const walletId = process.env.PRIVY_WALLET_ID;
|
|
1419
|
+
if (!appId) {
|
|
1420
|
+
throw new Error("PRIVY_APP_ID environment variable is required");
|
|
1421
|
+
}
|
|
1422
|
+
if (!appSecret) {
|
|
1423
|
+
throw new Error("PRIVY_APP_SECRET environment variable is required");
|
|
1424
|
+
}
|
|
1425
|
+
if (!walletId) {
|
|
1426
|
+
throw new Error("PRIVY_WALLET_ID environment variable is required");
|
|
1427
|
+
}
|
|
1428
|
+
return new _PrivyAdapter({
|
|
1429
|
+
appId,
|
|
1430
|
+
appSecret,
|
|
1431
|
+
walletId,
|
|
1432
|
+
baseUrl: process.env.PRIVY_API_BASE_URL
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
get baseUrl() {
|
|
1436
|
+
return this.config.baseUrl ?? PRIVY_API_BASE;
|
|
1437
|
+
}
|
|
1438
|
+
get authHeaders() {
|
|
1439
|
+
const credentials = Buffer.from(
|
|
1440
|
+
`${this.config.appId}:${this.config.appSecret}`
|
|
1441
|
+
).toString("base64");
|
|
1442
|
+
return {
|
|
1443
|
+
Authorization: `Basic ${credentials}`,
|
|
1444
|
+
"privy-app-id": this.config.appId,
|
|
1445
|
+
"Content-Type": "application/json"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async getAddress() {
|
|
1449
|
+
if (this.cachedAddress) return this.cachedAddress;
|
|
1450
|
+
const response = await fetch(
|
|
1451
|
+
`${this.baseUrl}/v1/wallets/${this.config.walletId}`,
|
|
1452
|
+
{ headers: this.authHeaders }
|
|
1453
|
+
);
|
|
1454
|
+
if (!response.ok) {
|
|
1455
|
+
const body = await response.text();
|
|
1456
|
+
throw new Error(`Privy getAddress failed (${response.status}): ${body}`);
|
|
1457
|
+
}
|
|
1458
|
+
const data = await response.json();
|
|
1459
|
+
this.cachedAddress = data.address;
|
|
1460
|
+
return data.address;
|
|
1461
|
+
}
|
|
1462
|
+
async sendTransaction(tx) {
|
|
1463
|
+
this.onRequest?.("sendTransaction", tx);
|
|
1464
|
+
const startTime = Date.now();
|
|
1465
|
+
const caip2 = `eip155:${tx.chainId}`;
|
|
1466
|
+
const response = await fetch(
|
|
1467
|
+
`${this.baseUrl}/v1/wallets/${this.config.walletId}/rpc`,
|
|
1468
|
+
{
|
|
1469
|
+
method: "POST",
|
|
1470
|
+
headers: this.authHeaders,
|
|
1471
|
+
body: JSON.stringify({
|
|
1472
|
+
method: "eth_sendTransaction",
|
|
1473
|
+
caip2,
|
|
1474
|
+
params: {
|
|
1475
|
+
transaction: {
|
|
1476
|
+
to: tx.to,
|
|
1477
|
+
data: tx.data,
|
|
1478
|
+
value: tx.value
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
})
|
|
1482
|
+
}
|
|
1483
|
+
);
|
|
1484
|
+
if (!response.ok) {
|
|
1485
|
+
const body = await response.text();
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`Privy sendTransaction failed (${response.status}): ${body}`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const data = await response.json();
|
|
1491
|
+
const result = { hash: data.data.hash };
|
|
1492
|
+
this.onResponse?.("sendTransaction", result, Date.now() - startTime);
|
|
1493
|
+
return result;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/wallet/turnkey.ts
|
|
1498
|
+
var TURNKEY_API_BASE = "https://api.turnkey.com";
|
|
1499
|
+
var TurnkeyAdapter = class _TurnkeyAdapter {
|
|
1500
|
+
name = "turnkey";
|
|
1501
|
+
onRequest;
|
|
1502
|
+
onResponse;
|
|
1503
|
+
config;
|
|
1504
|
+
constructor(config) {
|
|
1505
|
+
this.config = config;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Create a TurnkeyAdapter from environment variables.
|
|
1509
|
+
* Throws if any required variable is missing.
|
|
1510
|
+
*/
|
|
1511
|
+
static fromEnv() {
|
|
1512
|
+
const apiPublicKey = process.env.TURNKEY_API_PUBLIC_KEY;
|
|
1513
|
+
const apiPrivateKey = process.env.TURNKEY_API_PRIVATE_KEY;
|
|
1514
|
+
const organizationId = process.env.TURNKEY_ORGANIZATION_ID;
|
|
1515
|
+
const walletAddress = process.env.TURNKEY_WALLET_ADDRESS;
|
|
1516
|
+
if (!apiPublicKey) {
|
|
1517
|
+
throw new Error("TURNKEY_API_PUBLIC_KEY environment variable is required");
|
|
1518
|
+
}
|
|
1519
|
+
if (!apiPrivateKey) {
|
|
1520
|
+
throw new Error(
|
|
1521
|
+
"TURNKEY_API_PRIVATE_KEY environment variable is required"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (!organizationId) {
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
"TURNKEY_ORGANIZATION_ID environment variable is required"
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (!walletAddress) {
|
|
1530
|
+
throw new Error("TURNKEY_WALLET_ADDRESS environment variable is required");
|
|
1531
|
+
}
|
|
1532
|
+
const rpcUrl = process.env.TURNKEY_RPC_URL;
|
|
1533
|
+
if (!rpcUrl) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
"TURNKEY_RPC_URL environment variable is required. It is used for gas estimation and transaction broadcasting."
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
return new _TurnkeyAdapter({
|
|
1539
|
+
apiPublicKey,
|
|
1540
|
+
apiPrivateKey,
|
|
1541
|
+
organizationId,
|
|
1542
|
+
walletAddress,
|
|
1543
|
+
rpcUrl,
|
|
1544
|
+
privateKeyId: process.env.TURNKEY_PRIVATE_KEY_ID,
|
|
1545
|
+
baseUrl: process.env.TURNKEY_API_BASE_URL
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
get baseUrl() {
|
|
1549
|
+
return this.config.baseUrl ?? TURNKEY_API_BASE;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Sign a Turnkey API request using the API key pair (P-256 ECDSA).
|
|
1553
|
+
*
|
|
1554
|
+
* Turnkey uses a stamp-based authentication scheme: the request body
|
|
1555
|
+
* is hashed with SHA-256 and signed with the P-256 private key. The
|
|
1556
|
+
* stamp JSON (publicKey + scheme + signature) is then base64url-encoded
|
|
1557
|
+
* and sent in the X-Stamp header.
|
|
1558
|
+
*
|
|
1559
|
+
* @see https://docs.turnkey.com/developer-tools/api-overview/stamps
|
|
1560
|
+
*/
|
|
1561
|
+
async stamp(body) {
|
|
1562
|
+
const encoder = new TextEncoder();
|
|
1563
|
+
const bodyHash = await crypto.subtle.digest("SHA-256", encoder.encode(body));
|
|
1564
|
+
const keyData = hexToBytes(this.config.apiPrivateKey);
|
|
1565
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
1566
|
+
"pkcs8",
|
|
1567
|
+
derEncodeP256PrivateKey(keyData),
|
|
1568
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1569
|
+
false,
|
|
1570
|
+
["sign"]
|
|
1571
|
+
);
|
|
1572
|
+
const p1363Sig = await crypto.subtle.sign(
|
|
1573
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1574
|
+
cryptoKey,
|
|
1575
|
+
bodyHash
|
|
1576
|
+
);
|
|
1577
|
+
const derSig = p1363ToDer(new Uint8Array(p1363Sig));
|
|
1578
|
+
const signatureHex = bytesToHex(derSig);
|
|
1579
|
+
const stampJson = JSON.stringify({
|
|
1580
|
+
publicKey: this.config.apiPublicKey,
|
|
1581
|
+
scheme: "SIGNATURE_SCHEME_TK_API_P256",
|
|
1582
|
+
signature: signatureHex
|
|
1583
|
+
});
|
|
1584
|
+
return Buffer.from(stampJson).toString("base64url");
|
|
1585
|
+
}
|
|
1586
|
+
async signedRequest(path, body) {
|
|
1587
|
+
const bodyStr = JSON.stringify(body);
|
|
1588
|
+
const stampValue = await this.stamp(bodyStr);
|
|
1589
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
headers: {
|
|
1592
|
+
"Content-Type": "application/json",
|
|
1593
|
+
"X-Stamp": stampValue
|
|
1594
|
+
},
|
|
1595
|
+
body: bodyStr
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
async getAddress() {
|
|
1599
|
+
return this.config.walletAddress;
|
|
1600
|
+
}
|
|
1601
|
+
async sendTransaction(tx) {
|
|
1602
|
+
this.onRequest?.("sendTransaction", tx);
|
|
1603
|
+
const startTime = Date.now();
|
|
1604
|
+
const { rpcUrl } = this.config;
|
|
1605
|
+
const gasParams = await this.estimateGasParams(rpcUrl, tx);
|
|
1606
|
+
const rlpHex = rlpEncodeEip1559Tx({
|
|
1607
|
+
chainId: tx.chainId,
|
|
1608
|
+
nonce: gasParams.nonce,
|
|
1609
|
+
maxPriorityFeePerGas: gasParams.maxPriorityFeePerGas,
|
|
1610
|
+
maxFeePerGas: gasParams.maxFeePerGas,
|
|
1611
|
+
gasLimit: gasParams.gasLimit,
|
|
1612
|
+
to: tx.to,
|
|
1613
|
+
data: tx.data,
|
|
1614
|
+
value: tx.value
|
|
1615
|
+
});
|
|
1616
|
+
const signWith = this.config.privateKeyId ?? this.config.walletAddress;
|
|
1617
|
+
const response = await this.signedRequest(
|
|
1618
|
+
"/public/v1/submit/sign_transaction",
|
|
1619
|
+
{
|
|
1620
|
+
type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
|
|
1621
|
+
organizationId: this.config.organizationId,
|
|
1622
|
+
timestampMs: Date.now().toString(),
|
|
1623
|
+
parameters: {
|
|
1624
|
+
signWith,
|
|
1625
|
+
type: "TRANSACTION_TYPE_ETHEREUM",
|
|
1626
|
+
unsignedTransaction: rlpHex
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
if (!response.ok) {
|
|
1631
|
+
const body = await response.text();
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Turnkey sendTransaction failed (${response.status}): ${body}`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
const data = await response.json();
|
|
1637
|
+
const signedTx = data.activity.result?.signTransactionResult?.signedTransaction;
|
|
1638
|
+
if (!signedTx) {
|
|
1639
|
+
throw new Error(
|
|
1640
|
+
`Turnkey sign transaction did not return a signed payload (activity status: ${data.activity.status})`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const rpcResponse = await fetch(rpcUrl, {
|
|
1644
|
+
method: "POST",
|
|
1645
|
+
headers: { "Content-Type": "application/json" },
|
|
1646
|
+
body: JSON.stringify({
|
|
1647
|
+
jsonrpc: "2.0",
|
|
1648
|
+
id: 1,
|
|
1649
|
+
method: "eth_sendRawTransaction",
|
|
1650
|
+
params: [signedTx]
|
|
1651
|
+
})
|
|
1652
|
+
});
|
|
1653
|
+
if (!rpcResponse.ok) {
|
|
1654
|
+
const rpcBody = await rpcResponse.text();
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`Turnkey broadcast failed (${rpcResponse.status}): ${rpcBody}`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const rpcData = await rpcResponse.json();
|
|
1660
|
+
if (rpcData.error) {
|
|
1661
|
+
throw new Error(`Turnkey broadcast RPC error: ${rpcData.error.message}`);
|
|
1662
|
+
}
|
|
1663
|
+
if (!rpcData.result) {
|
|
1664
|
+
throw new Error("Turnkey broadcast returned no tx hash");
|
|
1665
|
+
}
|
|
1666
|
+
const result = { hash: rpcData.result };
|
|
1667
|
+
this.onResponse?.("sendTransaction", result, Date.now() - startTime);
|
|
1668
|
+
return result;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Populate gas parameters via JSON-RPC calls to the target chain.
|
|
1672
|
+
* Mirrors what ethers.js provider.populateTransaction() does internally.
|
|
1673
|
+
*
|
|
1674
|
+
* Makes three parallel RPC calls:
|
|
1675
|
+
* - eth_getTransactionCount (nonce)
|
|
1676
|
+
* - eth_estimateGas (gasLimit)
|
|
1677
|
+
* - eth_maxPriorityFeePerGas + eth_getBlockByNumber (fee data)
|
|
1678
|
+
*/
|
|
1679
|
+
async estimateGasParams(rpcUrl, tx) {
|
|
1680
|
+
const from = this.config.walletAddress;
|
|
1681
|
+
const txValue = tx.value === "0" ? "0x0" : `0x${BigInt(tx.value).toString(16)}`;
|
|
1682
|
+
const [nonceResult, gasEstimateResult, feeDataResult] = await Promise.all([
|
|
1683
|
+
this.rpcCall(rpcUrl, "eth_getTransactionCount", [from, "pending"]),
|
|
1684
|
+
this.rpcCall(rpcUrl, "eth_estimateGas", [
|
|
1685
|
+
{
|
|
1686
|
+
from,
|
|
1687
|
+
to: tx.to,
|
|
1688
|
+
data: tx.data || "0x",
|
|
1689
|
+
value: txValue
|
|
1690
|
+
}
|
|
1691
|
+
]),
|
|
1692
|
+
this.rpcCall(rpcUrl, "eth_feeHistory", [1, "latest", [50]])
|
|
1693
|
+
]);
|
|
1694
|
+
const nonce = BigInt(nonceResult);
|
|
1695
|
+
const rawGasLimit = BigInt(gasEstimateResult);
|
|
1696
|
+
const gasLimit = rawGasLimit * 120n / 100n;
|
|
1697
|
+
const feeHistory = feeDataResult;
|
|
1698
|
+
const latestBaseFee = BigInt(
|
|
1699
|
+
feeHistory.baseFeePerGas[1] ?? feeHistory.baseFeePerGas[0]
|
|
1700
|
+
);
|
|
1701
|
+
const maxPriorityFeePerGas = feeHistory.reward?.[0]?.[0] ? BigInt(feeHistory.reward[0][0]) : 1500000000n;
|
|
1702
|
+
const maxFeePerGas = latestBaseFee * 2n + maxPriorityFeePerGas;
|
|
1703
|
+
return { nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas };
|
|
1704
|
+
}
|
|
1705
|
+
/** Make a single JSON-RPC call */
|
|
1706
|
+
async rpcCall(rpcUrl, method, params) {
|
|
1707
|
+
const response = await fetch(rpcUrl, {
|
|
1708
|
+
method: "POST",
|
|
1709
|
+
headers: { "Content-Type": "application/json" },
|
|
1710
|
+
body: JSON.stringify({
|
|
1711
|
+
jsonrpc: "2.0",
|
|
1712
|
+
id: 1,
|
|
1713
|
+
method,
|
|
1714
|
+
params
|
|
1715
|
+
})
|
|
1716
|
+
});
|
|
1717
|
+
if (!response.ok) {
|
|
1718
|
+
const body = await response.text();
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`Turnkey RPC ${method} failed (${response.status}): ${body}`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
const data = await response.json();
|
|
1724
|
+
if (data.error) {
|
|
1725
|
+
throw new Error(`Turnkey RPC ${method} error: ${data.error.message}`);
|
|
1726
|
+
}
|
|
1727
|
+
return data.result;
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
function rlpEncodeEip1559Tx(tx) {
|
|
1731
|
+
const chainIdBytes = bigIntToBytes(BigInt(tx.chainId));
|
|
1732
|
+
const nonce = bigIntToBytes(tx.nonce);
|
|
1733
|
+
const maxPriorityFeePerGas = bigIntToBytes(tx.maxPriorityFeePerGas);
|
|
1734
|
+
const maxFeePerGas = bigIntToBytes(tx.maxFeePerGas);
|
|
1735
|
+
const gasLimit = bigIntToBytes(tx.gasLimit);
|
|
1736
|
+
const toBytes = hexToBytes(tx.to);
|
|
1737
|
+
const valueBytes = tx.value === "0" ? new Uint8Array(0) : bigIntToBytes(BigInt(tx.value));
|
|
1738
|
+
const dataBytes = tx.data ? hexToBytes(tx.data) : new Uint8Array(0);
|
|
1739
|
+
const fields = [
|
|
1740
|
+
rlpEncodeBytes(chainIdBytes),
|
|
1741
|
+
rlpEncodeBytes(nonce),
|
|
1742
|
+
rlpEncodeBytes(maxPriorityFeePerGas),
|
|
1743
|
+
rlpEncodeBytes(maxFeePerGas),
|
|
1744
|
+
rlpEncodeBytes(gasLimit),
|
|
1745
|
+
rlpEncodeBytes(toBytes),
|
|
1746
|
+
rlpEncodeBytes(valueBytes),
|
|
1747
|
+
rlpEncodeBytes(dataBytes),
|
|
1748
|
+
rlpEncodeList([])
|
|
1749
|
+
// empty access list
|
|
1750
|
+
];
|
|
1751
|
+
const rlpList = rlpEncodeList(fields);
|
|
1752
|
+
const result = new Uint8Array(1 + rlpList.length);
|
|
1753
|
+
result[0] = 2;
|
|
1754
|
+
result.set(rlpList, 1);
|
|
1755
|
+
return bytesToHex(result);
|
|
1756
|
+
}
|
|
1757
|
+
function rlpEncodeBytes(bytes) {
|
|
1758
|
+
if (bytes.length === 1 && bytes[0] < 128) {
|
|
1759
|
+
return bytes;
|
|
1760
|
+
}
|
|
1761
|
+
if (bytes.length === 0) {
|
|
1762
|
+
return new Uint8Array([128]);
|
|
1763
|
+
}
|
|
1764
|
+
if (bytes.length <= 55) {
|
|
1765
|
+
const result2 = new Uint8Array(1 + bytes.length);
|
|
1766
|
+
result2[0] = 128 + bytes.length;
|
|
1767
|
+
result2.set(bytes, 1);
|
|
1768
|
+
return result2;
|
|
1769
|
+
}
|
|
1770
|
+
const lenBytes = bigIntToBytes(BigInt(bytes.length));
|
|
1771
|
+
const result = new Uint8Array(1 + lenBytes.length + bytes.length);
|
|
1772
|
+
result[0] = 183 + lenBytes.length;
|
|
1773
|
+
result.set(lenBytes, 1);
|
|
1774
|
+
result.set(bytes, 1 + lenBytes.length);
|
|
1775
|
+
return result;
|
|
1776
|
+
}
|
|
1777
|
+
function rlpEncodeList(items) {
|
|
1778
|
+
let totalLen = 0;
|
|
1779
|
+
for (const item of items) totalLen += item.length;
|
|
1780
|
+
if (totalLen <= 55) {
|
|
1781
|
+
const result2 = new Uint8Array(1 + totalLen);
|
|
1782
|
+
result2[0] = 192 + totalLen;
|
|
1783
|
+
let offset2 = 1;
|
|
1784
|
+
for (const item of items) {
|
|
1785
|
+
result2.set(item, offset2);
|
|
1786
|
+
offset2 += item.length;
|
|
1787
|
+
}
|
|
1788
|
+
return result2;
|
|
1789
|
+
}
|
|
1790
|
+
const lenBytes = bigIntToBytes(BigInt(totalLen));
|
|
1791
|
+
const result = new Uint8Array(1 + lenBytes.length + totalLen);
|
|
1792
|
+
result[0] = 247 + lenBytes.length;
|
|
1793
|
+
result.set(lenBytes, 1);
|
|
1794
|
+
let offset = 1 + lenBytes.length;
|
|
1795
|
+
for (const item of items) {
|
|
1796
|
+
result.set(item, offset);
|
|
1797
|
+
offset += item.length;
|
|
1798
|
+
}
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
function bigIntToBytes(value) {
|
|
1802
|
+
if (value === 0n) return new Uint8Array(0);
|
|
1803
|
+
const hex = value.toString(16);
|
|
1804
|
+
const padded = hex.length % 2 === 0 ? hex : `0${hex}`;
|
|
1805
|
+
return hexToBytes(padded);
|
|
1806
|
+
}
|
|
1807
|
+
function hexToBytes(hex) {
|
|
1808
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1809
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
1810
|
+
for (let i = 0; i < clean.length; i += 2) {
|
|
1811
|
+
bytes[i / 2] = Number.parseInt(clean.slice(i, i + 2), 16);
|
|
1812
|
+
}
|
|
1813
|
+
return bytes;
|
|
1814
|
+
}
|
|
1815
|
+
function bytesToHex(bytes) {
|
|
1816
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1817
|
+
}
|
|
1818
|
+
function p1363ToDer(p1363) {
|
|
1819
|
+
const r = p1363.subarray(0, 32);
|
|
1820
|
+
const s = p1363.subarray(32, 64);
|
|
1821
|
+
const rDer = integerToDer(r);
|
|
1822
|
+
const sDer = integerToDer(s);
|
|
1823
|
+
const seqLen = rDer.length + sDer.length;
|
|
1824
|
+
const result = new Uint8Array(2 + seqLen);
|
|
1825
|
+
result[0] = 48;
|
|
1826
|
+
result[1] = seqLen;
|
|
1827
|
+
result.set(rDer, 2);
|
|
1828
|
+
result.set(sDer, 2 + rDer.length);
|
|
1829
|
+
return result;
|
|
1830
|
+
}
|
|
1831
|
+
function integerToDer(bytes) {
|
|
1832
|
+
let start = 0;
|
|
1833
|
+
while (start < bytes.length - 1 && bytes[start] === 0) start++;
|
|
1834
|
+
const stripped = bytes.subarray(start);
|
|
1835
|
+
const needsPad = stripped[0] >= 128;
|
|
1836
|
+
const len = stripped.length + (needsPad ? 1 : 0);
|
|
1837
|
+
const result = new Uint8Array(2 + len);
|
|
1838
|
+
result[0] = 2;
|
|
1839
|
+
result[1] = len;
|
|
1840
|
+
if (needsPad) {
|
|
1841
|
+
result[2] = 0;
|
|
1842
|
+
result.set(stripped, 3);
|
|
1843
|
+
} else {
|
|
1844
|
+
result.set(stripped, 2);
|
|
1845
|
+
}
|
|
1846
|
+
return result;
|
|
1847
|
+
}
|
|
1848
|
+
function derEncodeP256PrivateKey(rawKey) {
|
|
1849
|
+
const header = new Uint8Array([
|
|
1850
|
+
48,
|
|
1851
|
+
65,
|
|
1852
|
+
// SEQUENCE (65 bytes)
|
|
1853
|
+
2,
|
|
1854
|
+
1,
|
|
1855
|
+
0,
|
|
1856
|
+
// INTEGER 0 (version)
|
|
1857
|
+
48,
|
|
1858
|
+
19,
|
|
1859
|
+
// SEQUENCE (19 bytes) - AlgorithmIdentifier
|
|
1860
|
+
6,
|
|
1861
|
+
7,
|
|
1862
|
+
// OID (7 bytes) - id-ecPublicKey
|
|
1863
|
+
42,
|
|
1864
|
+
134,
|
|
1865
|
+
72,
|
|
1866
|
+
206,
|
|
1867
|
+
61,
|
|
1868
|
+
2,
|
|
1869
|
+
1,
|
|
1870
|
+
6,
|
|
1871
|
+
8,
|
|
1872
|
+
// OID (8 bytes) - secp256r1
|
|
1873
|
+
42,
|
|
1874
|
+
134,
|
|
1875
|
+
72,
|
|
1876
|
+
206,
|
|
1877
|
+
61,
|
|
1878
|
+
3,
|
|
1879
|
+
1,
|
|
1880
|
+
7,
|
|
1881
|
+
4,
|
|
1882
|
+
39,
|
|
1883
|
+
// OCTET STRING (39 bytes)
|
|
1884
|
+
48,
|
|
1885
|
+
37,
|
|
1886
|
+
// SEQUENCE (37 bytes)
|
|
1887
|
+
2,
|
|
1888
|
+
1,
|
|
1889
|
+
1,
|
|
1890
|
+
// INTEGER 1 (version)
|
|
1891
|
+
4,
|
|
1892
|
+
32
|
|
1893
|
+
// OCTET STRING (32 bytes) - private key
|
|
1894
|
+
]);
|
|
1895
|
+
const result = new Uint8Array(header.length + rawKey.length);
|
|
1896
|
+
result.set(header);
|
|
1897
|
+
result.set(rawKey, header.length);
|
|
1898
|
+
return result.buffer;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/wallet/chains.generated.ts
|
|
1902
|
+
var CHAIN_IDS = {
|
|
1903
|
+
ethereum: 1,
|
|
1904
|
+
mainnet: 1,
|
|
1905
|
+
optimism: 10,
|
|
1906
|
+
unichain: 130,
|
|
1907
|
+
polygon: 137,
|
|
1908
|
+
matic: 137,
|
|
1909
|
+
monad: 143,
|
|
1910
|
+
shape: 360,
|
|
1911
|
+
flow: 747,
|
|
1912
|
+
hyperevm: 999,
|
|
1913
|
+
sei: 1329,
|
|
1914
|
+
soneium: 1868,
|
|
1915
|
+
ronin: 2020,
|
|
1916
|
+
abstract: 2741,
|
|
1917
|
+
megaeth: 4326,
|
|
1918
|
+
somnia: 5031,
|
|
1919
|
+
b3: 8333,
|
|
1920
|
+
base: 8453,
|
|
1921
|
+
ape_chain: 33139,
|
|
1922
|
+
apechain: 33139,
|
|
1923
|
+
arbitrum: 42161,
|
|
1924
|
+
avalanche: 43114,
|
|
1925
|
+
gunzilla: 43419,
|
|
1926
|
+
ink: 57073,
|
|
1927
|
+
animechain: 69e3,
|
|
1928
|
+
bera_chain: 80094,
|
|
1929
|
+
berachain: 80094,
|
|
1930
|
+
blast: 81457,
|
|
1931
|
+
zora: 7777777
|
|
1932
|
+
};
|
|
1933
|
+
function resolveChainId(chain) {
|
|
1934
|
+
if (typeof chain === "number") return chain;
|
|
1935
|
+
const asNum = Number(chain);
|
|
1936
|
+
if (!Number.isNaN(asNum) && Number.isInteger(asNum)) return asNum;
|
|
1937
|
+
const id = CHAIN_IDS[chain];
|
|
1938
|
+
if (id === void 0) {
|
|
1939
|
+
throw new Error(
|
|
1940
|
+
`Unknown chain "${chain}". Pass a numeric chain ID or use a known name: ${Object.keys(CHAIN_IDS).join(", ")}`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
return id;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/wallet/index.ts
|
|
1947
|
+
var WALLET_PROVIDERS = [
|
|
1948
|
+
"privy",
|
|
1949
|
+
"turnkey",
|
|
1950
|
+
"fireblocks",
|
|
1951
|
+
"private-key"
|
|
1952
|
+
];
|
|
1953
|
+
function createWalletFromEnv(provider) {
|
|
1954
|
+
if (provider) {
|
|
1955
|
+
return createAdapter(provider);
|
|
1956
|
+
}
|
|
1957
|
+
const hasTurnkey = !!process.env.TURNKEY_API_PUBLIC_KEY && !!process.env.TURNKEY_ORGANIZATION_ID;
|
|
1958
|
+
const hasFireblocks = !!process.env.FIREBLOCKS_API_KEY && !!process.env.FIREBLOCKS_VAULT_ID;
|
|
1959
|
+
const hasPrivateKey = !!process.env.PRIVATE_KEY && !!process.env.RPC_URL;
|
|
1960
|
+
const hasPrivy = !!process.env.PRIVY_APP_ID && !!process.env.PRIVY_APP_SECRET;
|
|
1961
|
+
const detected = [
|
|
1962
|
+
hasTurnkey && "turnkey",
|
|
1963
|
+
hasFireblocks && "fireblocks",
|
|
1964
|
+
hasPrivateKey && "private-key",
|
|
1965
|
+
hasPrivy && "privy"
|
|
1966
|
+
].filter(Boolean);
|
|
1967
|
+
if (detected.length > 1) {
|
|
1968
|
+
console.warn(
|
|
1969
|
+
`WARNING: Multiple wallet providers detected: ${detected.join(", ")}. Using ${detected[0]}. Set --wallet-provider explicitly to avoid ambiguity.`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
if (hasTurnkey) return TurnkeyAdapter.fromEnv();
|
|
1973
|
+
if (hasFireblocks) return FireblocksAdapter.fromEnv();
|
|
1974
|
+
if (hasPrivateKey) return PrivateKeyAdapter.fromEnv();
|
|
1975
|
+
return PrivyAdapter.fromEnv();
|
|
1976
|
+
}
|
|
1977
|
+
function createAdapter(provider) {
|
|
1978
|
+
switch (provider) {
|
|
1979
|
+
case "privy":
|
|
1980
|
+
return PrivyAdapter.fromEnv();
|
|
1981
|
+
case "turnkey":
|
|
1982
|
+
return TurnkeyAdapter.fromEnv();
|
|
1983
|
+
case "fireblocks":
|
|
1984
|
+
return FireblocksAdapter.fromEnv();
|
|
1985
|
+
case "private-key":
|
|
1986
|
+
return PrivateKeyAdapter.fromEnv();
|
|
1987
|
+
default:
|
|
1988
|
+
throw new Error(
|
|
1989
|
+
`Unknown wallet provider "${provider}". Valid providers: ${WALLET_PROVIDERS.join(", ")}`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/sdk.ts
|
|
1995
|
+
var SwapsAPI = class {
|
|
1996
|
+
constructor(client) {
|
|
1997
|
+
this.client = client;
|
|
1998
|
+
}
|
|
1999
|
+
client;
|
|
2000
|
+
async quote(options) {
|
|
2001
|
+
return this.client.get("/api/v2/swap/quote", {
|
|
2002
|
+
from_chain: options.fromChain,
|
|
2003
|
+
from_address: options.fromAddress,
|
|
2004
|
+
to_chain: options.toChain,
|
|
2005
|
+
to_address: options.toAddress,
|
|
2006
|
+
quantity: options.quantity,
|
|
2007
|
+
address: options.address,
|
|
2008
|
+
slippage: options.slippage,
|
|
2009
|
+
recipient: options.recipient
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Get a swap quote and execute all transactions using the provided wallet adapter.
|
|
2014
|
+
* Returns an array of transaction results (one per transaction in the quote).
|
|
2015
|
+
*
|
|
2016
|
+
* @param options - Swap parameters (chains, addresses, quantity, etc.)
|
|
2017
|
+
* @param wallet - Wallet adapter to sign and send transactions
|
|
2018
|
+
* @param callbacks - Optional callbacks for progress reporting and skipped txs
|
|
2019
|
+
*/
|
|
2020
|
+
async execute(options, wallet, callbacks) {
|
|
2021
|
+
const address = options.address ?? await wallet.getAddress();
|
|
2022
|
+
const quote = await this.quote({ ...options, address });
|
|
2023
|
+
callbacks?.onQuote?.(quote);
|
|
2024
|
+
if (!quote.transactions || quote.transactions.length === 0) {
|
|
2025
|
+
throw new Error(
|
|
2026
|
+
"Swap quote returned zero transactions \u2014 the swap may not be available for these tokens/chains."
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
const results = [];
|
|
2030
|
+
for (const tx of quote.transactions) {
|
|
2031
|
+
if (!tx.to) {
|
|
2032
|
+
callbacks?.onSkipped?.({
|
|
2033
|
+
chain: tx.chain,
|
|
2034
|
+
reason: "missing 'to' address"
|
|
2035
|
+
});
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const chainId = resolveChainId(tx.chain);
|
|
2039
|
+
callbacks?.onSending?.({ to: tx.to, chain: tx.chain, chainId });
|
|
2040
|
+
const result = await wallet.sendTransaction({
|
|
2041
|
+
to: tx.to,
|
|
2042
|
+
data: tx.data,
|
|
2043
|
+
value: tx.value ?? "0",
|
|
2044
|
+
chainId
|
|
2045
|
+
});
|
|
2046
|
+
results.push(result);
|
|
2047
|
+
}
|
|
2048
|
+
if (results.length === 0) {
|
|
2049
|
+
throw new Error(
|
|
2050
|
+
"All swap transactions were skipped (no valid 'to' addresses). The quote may be malformed."
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
return results;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
// src/commands/swaps.ts
|
|
733
2058
|
function swapsCommand(getClient2, getFormat2) {
|
|
734
|
-
const cmd = new
|
|
735
|
-
"Get swap quotes
|
|
2059
|
+
const cmd = new Command11("swaps").description(
|
|
2060
|
+
"Get swap quotes and execute token swaps"
|
|
736
2061
|
);
|
|
737
2062
|
cmd.command("quote").description(
|
|
738
2063
|
"Get a quote for swapping tokens, including price details and executable transaction data"
|
|
@@ -767,13 +2092,73 @@ function swapsCommand(getClient2, getFormat2) {
|
|
|
767
2092
|
console.log(formatOutput(result, getFormat2()));
|
|
768
2093
|
}
|
|
769
2094
|
);
|
|
2095
|
+
cmd.command("execute").description(
|
|
2096
|
+
"Get a swap quote and execute it onchain using a managed wallet. Supports Privy (default), Turnkey, and Fireblocks providers."
|
|
2097
|
+
).requiredOption("--from-chain <chain>", "Chain of the token to swap from").requiredOption(
|
|
2098
|
+
"--from-address <address>",
|
|
2099
|
+
"Contract address of the token to swap from"
|
|
2100
|
+
).requiredOption("--to-chain <chain>", "Chain of the token to swap to").requiredOption(
|
|
2101
|
+
"--to-address <address>",
|
|
2102
|
+
"Contract address of the token to swap to"
|
|
2103
|
+
).requiredOption("--quantity <quantity>", "Amount to swap (in token units)").option(
|
|
2104
|
+
"--slippage <slippage>",
|
|
2105
|
+
"Slippage tolerance (0.0 to 0.5, default: 0.01)"
|
|
2106
|
+
).option(
|
|
2107
|
+
"--recipient <recipient>",
|
|
2108
|
+
"Recipient address (defaults to wallet address)"
|
|
2109
|
+
).option(
|
|
2110
|
+
"--wallet-provider <provider>",
|
|
2111
|
+
`Wallet provider to use (${WALLET_PROVIDERS.join(", ")})`
|
|
2112
|
+
).option("--dry-run", "Print quote and transaction details without signing").action(
|
|
2113
|
+
async (options) => {
|
|
2114
|
+
const wallet = createWalletFromEnv(
|
|
2115
|
+
options.walletProvider
|
|
2116
|
+
);
|
|
2117
|
+
const address = await wallet.getAddress();
|
|
2118
|
+
console.error(`Using ${wallet.name} wallet: ${address}`);
|
|
2119
|
+
const swaps = new SwapsAPI(getClient2());
|
|
2120
|
+
const format = getFormat2();
|
|
2121
|
+
const slippage = options.slippage ? parseFloatOption(options.slippage, "--slippage") : void 0;
|
|
2122
|
+
if (options.dryRun) {
|
|
2123
|
+
const quote = await swaps.quote({
|
|
2124
|
+
...options,
|
|
2125
|
+
address,
|
|
2126
|
+
slippage
|
|
2127
|
+
});
|
|
2128
|
+
console.log(formatOutput(quote, format));
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const results = await swaps.execute(
|
|
2132
|
+
{
|
|
2133
|
+
...options,
|
|
2134
|
+
address,
|
|
2135
|
+
slippage
|
|
2136
|
+
},
|
|
2137
|
+
wallet,
|
|
2138
|
+
{
|
|
2139
|
+
onQuote: () => console.error(
|
|
2140
|
+
`Quote: ${options.quantity} on ${options.fromChain} \u2192 ${options.toChain}`
|
|
2141
|
+
),
|
|
2142
|
+
onSending: (tx) => console.error(
|
|
2143
|
+
`Sending transaction to ${tx.to} on chain ${tx.chain} (${tx.chainId})...`
|
|
2144
|
+
),
|
|
2145
|
+
onSkipped: (tx) => console.error(
|
|
2146
|
+
`Skipping transaction on ${tx.chain}: ${tx.reason}`
|
|
2147
|
+
)
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
2150
|
+
for (const result of results) {
|
|
2151
|
+
console.log(formatOutput({ hash: result.hash }, format));
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
);
|
|
770
2155
|
return cmd;
|
|
771
2156
|
}
|
|
772
2157
|
|
|
773
2158
|
// src/commands/tokens.ts
|
|
774
|
-
import { Command as
|
|
2159
|
+
import { Command as Command12 } from "commander";
|
|
775
2160
|
function tokensCommand(getClient2, getFormat2) {
|
|
776
|
-
const cmd = new
|
|
2161
|
+
const cmd = new Command12("tokens").description(
|
|
777
2162
|
"Query trending tokens, top tokens, and token details"
|
|
778
2163
|
);
|
|
779
2164
|
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(
|
|
@@ -817,6 +2202,9 @@ function tokensCommand(getClient2, getFormat2) {
|
|
|
817
2202
|
}
|
|
818
2203
|
|
|
819
2204
|
// src/cli.ts
|
|
2205
|
+
var EXIT_API_ERROR = 1;
|
|
2206
|
+
var EXIT_AUTH_ERROR = 2;
|
|
2207
|
+
var EXIT_RATE_LIMITED = 3;
|
|
820
2208
|
var BANNER = `
|
|
821
2209
|
____ _____
|
|
822
2210
|
/ __ \\ / ____|
|
|
@@ -827,8 +2215,11 @@ var BANNER = `
|
|
|
827
2215
|
| |
|
|
828
2216
|
|_|
|
|
829
2217
|
`;
|
|
830
|
-
var program = new
|
|
831
|
-
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")
|
|
2218
|
+
var program = new Command13();
|
|
2219
|
+
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(
|
|
2220
|
+
"--fields <fields>",
|
|
2221
|
+
"Comma-separated list of fields to include in output"
|
|
2222
|
+
).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");
|
|
832
2223
|
function getClient() {
|
|
833
2224
|
const opts = program.opts();
|
|
834
2225
|
const apiKey = opts.apiKey ?? process.env.OPENSEA_API_KEY;
|
|
@@ -836,14 +2227,16 @@ function getClient() {
|
|
|
836
2227
|
console.error(
|
|
837
2228
|
"Error: API key required. Use --api-key or set OPENSEA_API_KEY environment variable."
|
|
838
2229
|
);
|
|
839
|
-
process.exit(
|
|
2230
|
+
process.exit(EXIT_AUTH_ERROR);
|
|
840
2231
|
}
|
|
2232
|
+
const maxRetries = opts.retry ? parseIntOption(opts.maxRetries, "--max-retries") : 0;
|
|
841
2233
|
return new OpenSeaClient({
|
|
842
2234
|
apiKey,
|
|
843
2235
|
chain: opts.chain,
|
|
844
2236
|
baseUrl: opts.baseUrl,
|
|
845
2237
|
timeout: parseIntOption(opts.timeout, "--timeout"),
|
|
846
|
-
verbose: opts.verbose
|
|
2238
|
+
verbose: opts.verbose,
|
|
2239
|
+
maxRetries
|
|
847
2240
|
});
|
|
848
2241
|
}
|
|
849
2242
|
function getFormat() {
|
|
@@ -852,7 +2245,24 @@ function getFormat() {
|
|
|
852
2245
|
if (opts.format === "toon") return "toon";
|
|
853
2246
|
return "json";
|
|
854
2247
|
}
|
|
2248
|
+
program.hook("preAction", () => {
|
|
2249
|
+
const opts = program.opts();
|
|
2250
|
+
let maxLines;
|
|
2251
|
+
if (opts.maxLines) {
|
|
2252
|
+
maxLines = parseIntOption(opts.maxLines, "--max-lines");
|
|
2253
|
+
if (maxLines < 1) {
|
|
2254
|
+
console.error("Error: --max-lines must be >= 1");
|
|
2255
|
+
process.exit(2);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
setOutputOptions({
|
|
2259
|
+
fields: opts.fields?.split(",").map((f) => f.trim()),
|
|
2260
|
+
maxLines
|
|
2261
|
+
});
|
|
2262
|
+
});
|
|
2263
|
+
program.addCommand(chainsCommand(getClient, getFormat));
|
|
855
2264
|
program.addCommand(collectionsCommand(getClient, getFormat));
|
|
2265
|
+
program.addCommand(dropsCommand(getClient, getFormat));
|
|
856
2266
|
program.addCommand(nftsCommand(getClient, getFormat));
|
|
857
2267
|
program.addCommand(listingsCommand(getClient, getFormat));
|
|
858
2268
|
program.addCommand(offersCommand(getClient, getFormat));
|
|
@@ -861,15 +2271,17 @@ program.addCommand(accountsCommand(getClient, getFormat));
|
|
|
861
2271
|
program.addCommand(tokensCommand(getClient, getFormat));
|
|
862
2272
|
program.addCommand(searchCommand(getClient, getFormat));
|
|
863
2273
|
program.addCommand(swapsCommand(getClient, getFormat));
|
|
2274
|
+
program.addCommand(healthCommand(getClient, getFormat));
|
|
864
2275
|
async function main() {
|
|
865
2276
|
try {
|
|
866
2277
|
await program.parseAsync(process.argv);
|
|
867
2278
|
} catch (error) {
|
|
868
2279
|
if (error instanceof OpenSeaAPIError) {
|
|
2280
|
+
const isRateLimited = error.statusCode === 429;
|
|
869
2281
|
console.error(
|
|
870
2282
|
JSON.stringify(
|
|
871
2283
|
{
|
|
872
|
-
error: "API Error",
|
|
2284
|
+
error: isRateLimited ? "Rate Limited" : "API Error",
|
|
873
2285
|
status: error.statusCode,
|
|
874
2286
|
path: error.path,
|
|
875
2287
|
message: error.responseBody
|
|
@@ -878,7 +2290,7 @@ async function main() {
|
|
|
878
2290
|
2
|
|
879
2291
|
)
|
|
880
2292
|
);
|
|
881
|
-
process.exit(
|
|
2293
|
+
process.exit(isRateLimited ? EXIT_RATE_LIMITED : EXIT_API_ERROR);
|
|
882
2294
|
}
|
|
883
2295
|
const label = error instanceof TypeError ? "Network Error" : error.name;
|
|
884
2296
|
console.error(
|
|
@@ -891,7 +2303,7 @@ async function main() {
|
|
|
891
2303
|
2
|
|
892
2304
|
)
|
|
893
2305
|
);
|
|
894
|
-
process.exit(
|
|
2306
|
+
process.exit(EXIT_API_ERROR);
|
|
895
2307
|
}
|
|
896
2308
|
}
|
|
897
2309
|
main();
|