@opensea/cli 0.4.2 → 1.0.1

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,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command11 } 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/${"0.4.2"}`;
9
+ var USER_AGENT = `opensea-cli/${"1.0.1"}`;
10
10
  var DEFAULT_MAX_RETRIES = 0;
11
11
  var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
12
12
  function isRetryableStatus(status, method) {
@@ -149,6 +149,9 @@ var OpenSeaAPIError = class extends Error {
149
149
  this.path = path;
150
150
  this.name = "OpenSeaAPIError";
151
151
  }
152
+ statusCode;
153
+ responseBody;
154
+ path;
152
155
  };
153
156
 
154
157
  // src/commands/accounts.ts
@@ -504,20 +507,6 @@ function truncateOutput(text, maxLines) {
504
507
  ... (${omitted} more line${omitted === 1 ? "" : "s"})`;
505
508
  }
506
509
 
507
- // src/commands/accounts.ts
508
- function accountsCommand(getClient2, getFormat2) {
509
- const cmd = new Command("accounts").description("Query accounts");
510
- cmd.command("get").description("Get an account by address").argument("<address>", "Wallet address").action(async (address) => {
511
- const client = getClient2();
512
- const result = await client.get(`/api/v2/accounts/${address}`);
513
- console.log(formatOutput(result, getFormat2()));
514
- });
515
- return cmd;
516
- }
517
-
518
- // src/commands/collections.ts
519
- import { Command as Command2 } from "commander";
520
-
521
510
  // src/parse.ts
522
511
  function parseIntOption(value, name) {
523
512
  const parsed = Number.parseInt(value, 10);
@@ -534,9 +523,65 @@ function parseFloatOption(value, name) {
534
523
  return parsed;
535
524
  }
536
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
+
537
581
  // src/commands/collections.ts
582
+ import { Command as Command3 } from "commander";
538
583
  function collectionsCommand(getClient2, getFormat2) {
539
- const cmd = new Command2("collections").description(
584
+ const cmd = new Command3("collections").description(
540
585
  "Manage and query NFT collections"
541
586
  );
542
587
  cmd.command("get").description("Get a single collection by slug").argument("<slug>", "Collection slug").action(async (slug) => {
@@ -575,13 +620,105 @@ function collectionsCommand(getClient2, getFormat2) {
575
620
  );
576
621
  console.log(formatOutput(result, getFormat2()));
577
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
+ );
578
715
  return cmd;
579
716
  }
580
717
 
581
718
  // src/commands/events.ts
582
- import { Command as Command3 } from "commander";
719
+ import { Command as Command5 } from "commander";
583
720
  function eventsCommand(getClient2, getFormat2) {
584
- const cmd = new Command3("events").description("Query marketplace events");
721
+ const cmd = new Command5("events").description("Query marketplace events");
585
722
  cmd.command("list").description("List events").option(
586
723
  "--event-type <type>",
587
724
  "Event type (sale, transfer, mint, listing, offer, trait_offer, collection_offer)"
@@ -640,7 +777,7 @@ function eventsCommand(getClient2, getFormat2) {
640
777
  }
641
778
 
642
779
  // src/commands/health.ts
643
- import { Command as Command4 } from "commander";
780
+ import { Command as Command6 } from "commander";
644
781
 
645
782
  // src/health.ts
646
783
  async function checkHealth(client) {
@@ -706,7 +843,7 @@ async function checkHealth(client) {
706
843
 
707
844
  // src/commands/health.ts
708
845
  function healthCommand(getClient2, getFormat2) {
709
- const cmd = new Command4("health").description("Check API connectivity and authentication").action(async () => {
846
+ const cmd = new Command6("health").description("Check API connectivity and authentication").action(async () => {
710
847
  const client = getClient2();
711
848
  const result = await checkHealth(client);
712
849
  console.log(formatOutput(result, getFormat2()));
@@ -718,9 +855,9 @@ function healthCommand(getClient2, getFormat2) {
718
855
  }
719
856
 
720
857
  // src/commands/listings.ts
721
- import { Command as Command5 } from "commander";
858
+ import { Command as Command7 } from "commander";
722
859
  function listingsCommand(getClient2, getFormat2) {
723
- const cmd = new Command5("listings").description("Query NFT listings");
860
+ const cmd = new Command7("listings").description("Query NFT listings");
724
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(
725
862
  async (collection, options) => {
726
863
  const client = getClient2();
@@ -752,9 +889,9 @@ function listingsCommand(getClient2, getFormat2) {
752
889
  }
753
890
 
754
891
  // src/commands/nfts.ts
755
- import { Command as Command6 } from "commander";
892
+ import { Command as Command8 } from "commander";
756
893
  function nftsCommand(getClient2, getFormat2) {
757
- const cmd = new Command6("nfts").description("Query NFTs");
894
+ const cmd = new Command8("nfts").description("Query NFTs");
758
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) => {
759
896
  const client = getClient2();
760
897
  const result = await client.get(
@@ -818,13 +955,31 @@ function nftsCommand(getClient2, getFormat2) {
818
955
  );
819
956
  console.log(formatOutput(result, getFormat2()));
820
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
+ );
821
976
  return cmd;
822
977
  }
823
978
 
824
979
  // src/commands/offers.ts
825
- import { Command as Command7 } from "commander";
980
+ import { Command as Command9 } from "commander";
826
981
  function offersCommand(getClient2, getFormat2) {
827
- const cmd = new Command7("offers").description("Query NFT offers");
982
+ const cmd = new Command9("offers").description("Query NFT offers");
828
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(
829
984
  async (collection, options) => {
830
985
  const client = getClient2();
@@ -868,9 +1023,9 @@ function offersCommand(getClient2, getFormat2) {
868
1023
  }
869
1024
 
870
1025
  // src/commands/search.ts
871
- import { Command as Command8 } from "commander";
1026
+ import { Command as Command10 } from "commander";
872
1027
  function searchCommand(getClient2, getFormat2) {
873
- const cmd = new Command8("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(
874
1029
  "--types <types>",
875
1030
  "Filter by type (comma-separated: collection,nft,token,account)"
876
1031
  ).option("--chains <chains>", "Filter by chains (comma-separated)").option("--limit <limit>", "Number of results", "20").action(
@@ -897,10 +1052,1036 @@ function searchCommand(getClient2, getFormat2) {
897
1052
  }
898
1053
 
899
1054
  // src/commands/swaps.ts
900
- import { Command as Command9 } 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
+ function convertToSmallestUnit(amount, decimals) {
1996
+ const [whole = "0", frac = ""] = amount.split(".");
1997
+ if (frac.length > decimals) {
1998
+ throw new Error(
1999
+ `Too many decimal places (${frac.length}) for token with ${decimals} decimals`
2000
+ );
2001
+ }
2002
+ const paddedFrac = frac.padEnd(decimals, "0");
2003
+ return (BigInt(whole) * BigInt(10) ** BigInt(decimals) + BigInt(paddedFrac)).toString();
2004
+ }
2005
+ async function resolveQuantity(client, chain, tokenAddress, quantity) {
2006
+ if (/^\d+$/.test(quantity)) {
2007
+ return quantity;
2008
+ }
2009
+ if (!/^\d+\.\d+$/.test(quantity)) {
2010
+ throw new Error(
2011
+ `Invalid quantity "${quantity}": must be an integer or decimal number`
2012
+ );
2013
+ }
2014
+ const token = await client.get(
2015
+ `/api/v2/chain/${chain}/token/${tokenAddress}`
2016
+ );
2017
+ return convertToSmallestUnit(quantity, token.decimals);
2018
+ }
2019
+ var SwapsAPI = class {
2020
+ constructor(client) {
2021
+ this.client = client;
2022
+ }
2023
+ client;
2024
+ async quote(options) {
2025
+ return this.client.get("/api/v2/swap/quote", {
2026
+ from_chain: options.fromChain,
2027
+ from_address: options.fromAddress,
2028
+ to_chain: options.toChain,
2029
+ to_address: options.toAddress,
2030
+ quantity: options.quantity,
2031
+ address: options.address,
2032
+ slippage: options.slippage,
2033
+ recipient: options.recipient
2034
+ });
2035
+ }
2036
+ /**
2037
+ * Get a swap quote and execute all transactions using the provided wallet adapter.
2038
+ * Returns an array of transaction results (one per transaction in the quote).
2039
+ *
2040
+ * @param options - Swap parameters (chains, addresses, quantity, etc.)
2041
+ * @param wallet - Wallet adapter to sign and send transactions
2042
+ * @param callbacks - Optional callbacks for progress reporting and skipped txs
2043
+ */
2044
+ async execute(options, wallet, callbacks) {
2045
+ const address = options.address ?? await wallet.getAddress();
2046
+ const quote = await this.quote({ ...options, address });
2047
+ callbacks?.onQuote?.(quote);
2048
+ if (!quote.transactions || quote.transactions.length === 0) {
2049
+ throw new Error(
2050
+ "Swap quote returned zero transactions \u2014 the swap may not be available for these tokens/chains."
2051
+ );
2052
+ }
2053
+ const results = [];
2054
+ for (const tx of quote.transactions) {
2055
+ if (!tx.to) {
2056
+ callbacks?.onSkipped?.({
2057
+ chain: tx.chain,
2058
+ reason: "missing 'to' address"
2059
+ });
2060
+ continue;
2061
+ }
2062
+ const chainId = resolveChainId(tx.chain);
2063
+ callbacks?.onSending?.({ to: tx.to, chain: tx.chain, chainId });
2064
+ const result = await wallet.sendTransaction({
2065
+ to: tx.to,
2066
+ data: tx.data,
2067
+ value: tx.value ?? "0",
2068
+ chainId
2069
+ });
2070
+ results.push(result);
2071
+ }
2072
+ if (results.length === 0) {
2073
+ throw new Error(
2074
+ "All swap transactions were skipped (no valid 'to' addresses). The quote may be malformed."
2075
+ );
2076
+ }
2077
+ return results;
2078
+ }
2079
+ };
2080
+
2081
+ // src/commands/swaps.ts
901
2082
  function swapsCommand(getClient2, getFormat2) {
902
- const cmd = new Command9("swaps").description(
903
- "Get swap quotes for token trading"
2083
+ const cmd = new Command11("swaps").description(
2084
+ "Get swap quotes and execute token swaps"
904
2085
  );
905
2086
  cmd.command("quote").description(
906
2087
  "Get a quote for swapping tokens, including price details and executable transaction data"
@@ -910,7 +2091,10 @@ function swapsCommand(getClient2, getFormat2) {
910
2091
  ).requiredOption("--to-chain <chain>", "Chain of the token to swap to").requiredOption(
911
2092
  "--to-address <address>",
912
2093
  "Contract address of the token to swap to"
913
- ).requiredOption("--quantity <quantity>", "Amount to swap (in token units)").requiredOption("--address <address>", "Wallet address executing the swap").option(
2094
+ ).requiredOption(
2095
+ "--quantity <quantity>",
2096
+ "Amount to swap (decimals like 0.1 are auto-converted to smallest units)"
2097
+ ).requiredOption("--address <address>", "Wallet address executing the swap").option(
914
2098
  "--slippage <slippage>",
915
2099
  "Slippage tolerance (0.0 to 0.5, default: 0.01)"
916
2100
  ).option(
@@ -919,6 +2103,12 @@ function swapsCommand(getClient2, getFormat2) {
919
2103
  ).action(
920
2104
  async (options) => {
921
2105
  const client = getClient2();
2106
+ const quantity = await resolveQuantity(
2107
+ client,
2108
+ options.fromChain,
2109
+ options.fromAddress,
2110
+ options.quantity
2111
+ );
922
2112
  const result = await client.get(
923
2113
  "/api/v2/swap/quote",
924
2114
  {
@@ -926,7 +2116,7 @@ function swapsCommand(getClient2, getFormat2) {
926
2116
  from_address: options.fromAddress,
927
2117
  to_chain: options.toChain,
928
2118
  to_address: options.toAddress,
929
- quantity: options.quantity,
2119
+ quantity,
930
2120
  address: options.address,
931
2121
  slippage: options.slippage ? parseFloatOption(options.slippage, "--slippage") : void 0,
932
2122
  recipient: options.recipient
@@ -935,13 +2125,85 @@ function swapsCommand(getClient2, getFormat2) {
935
2125
  console.log(formatOutput(result, getFormat2()));
936
2126
  }
937
2127
  );
2128
+ cmd.command("execute").description(
2129
+ "Get a swap quote and execute it onchain using a managed wallet. Supports Privy (default), Turnkey, and Fireblocks providers."
2130
+ ).requiredOption("--from-chain <chain>", "Chain of the token to swap from").requiredOption(
2131
+ "--from-address <address>",
2132
+ "Contract address of the token to swap from"
2133
+ ).requiredOption("--to-chain <chain>", "Chain of the token to swap to").requiredOption(
2134
+ "--to-address <address>",
2135
+ "Contract address of the token to swap to"
2136
+ ).requiredOption(
2137
+ "--quantity <quantity>",
2138
+ "Amount to swap (decimals like 0.1 are auto-converted to smallest units)"
2139
+ ).option(
2140
+ "--slippage <slippage>",
2141
+ "Slippage tolerance (0.0 to 0.5, default: 0.01)"
2142
+ ).option(
2143
+ "--recipient <recipient>",
2144
+ "Recipient address (defaults to wallet address)"
2145
+ ).option(
2146
+ "--wallet-provider <provider>",
2147
+ `Wallet provider to use (${WALLET_PROVIDERS.join(", ")})`
2148
+ ).option("--dry-run", "Print quote and transaction details without signing").action(
2149
+ async (options) => {
2150
+ const wallet = createWalletFromEnv(
2151
+ options.walletProvider
2152
+ );
2153
+ const address = await wallet.getAddress();
2154
+ console.error(`Using ${wallet.name} wallet: ${address}`);
2155
+ const client = getClient2();
2156
+ const quantity = await resolveQuantity(
2157
+ client,
2158
+ options.fromChain,
2159
+ options.fromAddress,
2160
+ options.quantity
2161
+ );
2162
+ const swaps = new SwapsAPI(client);
2163
+ const format = getFormat2();
2164
+ const slippage = options.slippage ? parseFloatOption(options.slippage, "--slippage") : void 0;
2165
+ if (options.dryRun) {
2166
+ const quote = await swaps.quote({
2167
+ ...options,
2168
+ quantity,
2169
+ address,
2170
+ slippage
2171
+ });
2172
+ console.log(formatOutput(quote, format));
2173
+ return;
2174
+ }
2175
+ const results = await swaps.execute(
2176
+ {
2177
+ ...options,
2178
+ quantity,
2179
+ address,
2180
+ slippage
2181
+ },
2182
+ wallet,
2183
+ {
2184
+ onQuote: () => console.error(
2185
+ `Quote: ${options.quantity} on ${options.fromChain} \u2192 ${options.toChain}`
2186
+ ),
2187
+ onSending: (tx) => console.error(
2188
+ `Sending transaction to ${tx.to} on chain ${tx.chain} (${tx.chainId})...`
2189
+ ),
2190
+ onSkipped: (tx) => console.error(
2191
+ `Skipping transaction on ${tx.chain}: ${tx.reason}`
2192
+ )
2193
+ }
2194
+ );
2195
+ for (const result of results) {
2196
+ console.log(formatOutput({ hash: result.hash }, format));
2197
+ }
2198
+ }
2199
+ );
938
2200
  return cmd;
939
2201
  }
940
2202
 
941
2203
  // src/commands/tokens.ts
942
- import { Command as Command10 } from "commander";
2204
+ import { Command as Command12 } from "commander";
943
2205
  function tokensCommand(getClient2, getFormat2) {
944
- const cmd = new Command10("tokens").description(
2206
+ const cmd = new Command12("tokens").description(
945
2207
  "Query trending tokens, top tokens, and token details"
946
2208
  );
947
2209
  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(
@@ -998,8 +2260,8 @@ var BANNER = `
998
2260
  | |
999
2261
  |_|
1000
2262
  `;
1001
- var program = new Command11();
1002
- program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version(process.env.npm_package_version ?? "0.0.0").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr").option(
2263
+ var program = new Command13();
2264
+ program.name("opensea").description("OpenSea CLI - Query the OpenSea API from the command line").version("1.0.1").addHelpText("before", BANNER).option("--api-key <key>", "OpenSea API key (or set OPENSEA_API_KEY env var)").option("--chain <chain>", "Default chain", "ethereum").option("--format <format>", "Output format (json, table, or toon)", "json").option("--base-url <url>", "API base URL").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--verbose", "Log request and response info to stderr").option(
1003
2265
  "--fields <fields>",
1004
2266
  "Comma-separated list of fields to include in output"
1005
2267
  ).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");
@@ -1043,7 +2305,9 @@ program.hook("preAction", () => {
1043
2305
  maxLines
1044
2306
  });
1045
2307
  });
2308
+ program.addCommand(chainsCommand(getClient, getFormat));
1046
2309
  program.addCommand(collectionsCommand(getClient, getFormat));
2310
+ program.addCommand(dropsCommand(getClient, getFormat));
1047
2311
  program.addCommand(nftsCommand(getClient, getFormat));
1048
2312
  program.addCommand(listingsCommand(getClient, getFormat));
1049
2313
  program.addCommand(offersCommand(getClient, getFormat));