@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/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 Command10 } from "commander";
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 fetch(url.toString(), {
35
- method: "GET",
36
- headers: {
37
- Accept: "application/json",
38
- "x-api-key": this.apiKey
63
+ const response = await this.fetchWithRetry(
64
+ url.toString(),
65
+ {
66
+ method: "GET",
67
+ headers: this.defaultHeaders
39
68
  },
40
- signal: AbortSignal.timeout(this.timeoutMs)
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 fetch(url.toString(), {
71
- method: "POST",
72
- headers,
73
- body: body ? JSON.stringify(body) : void 0,
74
- signal: AbortSignal.timeout(this.timeoutMs)
75
- });
76
- if (this.verbose) {
77
- console.error(`[verbose] ${response.status} ${response.statusText}`);
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
- return formatTable(data);
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 (format === "toon") {
378
- return formatToon(data);
445
+ if (_outputOptions.maxLines != null) {
446
+ result = truncateOutput(result, _outputOptions.maxLines);
379
447
  }
380
- return JSON.stringify(data, null, 2);
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
- // src/commands/accounts.ts
418
- function accountsCommand(getClient2, getFormat2) {
419
- const cmd = new Command("accounts").description("Query accounts");
420
- cmd.command("get").description("Get an account by address").argument("<address>", "Wallet address").action(async (address) => {
421
- const client = getClient2();
422
- const result = await client.get(`/api/v2/accounts/${address}`);
423
- console.log(formatOutput(result, getFormat2()));
424
- });
425
- return cmd;
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 Command2("collections").description(
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 Command3 } from "commander";
719
+ import { Command as Command5 } from "commander";
493
720
  function eventsCommand(getClient2, getFormat2) {
494
- const cmd = new Command3("events").description("Query marketplace events");
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 Command4 } from "commander";
858
+ import { Command as Command7 } from "commander";
554
859
  function listingsCommand(getClient2, getFormat2) {
555
- const cmd = new Command4("listings").description("Query NFT listings");
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 Command5 } from "commander";
892
+ import { Command as Command8 } from "commander";
588
893
  function nftsCommand(getClient2, getFormat2) {
589
- const cmd = new Command5("nfts").description("Query NFTs");
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 Command6 } from "commander";
980
+ import { Command as Command9 } from "commander";
658
981
  function offersCommand(getClient2, getFormat2) {
659
- const cmd = new Command6("offers").description("Query NFT offers");
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 Command7 } from "commander";
1026
+ import { Command as Command10 } from "commander";
704
1027
  function searchCommand(getClient2, getFormat2) {
705
- const cmd = new Command7("search").description("Search across collections, tokens, NFTs, and accounts").argument("<query>", "Search query").option(
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 Command8 } from "commander";
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 Command8("swaps").description(
735
- "Get swap quotes for token trading"
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 Command9 } from "commander";
2159
+ import { Command as Command12 } from "commander";
775
2160
  function tokensCommand(getClient2, getFormat2) {
776
- const cmd = new Command9("tokens").description(
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 Command10();
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(2);
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(1);
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(1);
2306
+ process.exit(EXIT_API_ERROR);
895
2307
  }
896
2308
  }
897
2309
  main();