@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/index.js CHANGED
@@ -1,18 +1,47 @@
1
1
  // src/client.ts
2
2
  var DEFAULT_BASE_URL = "https://api.opensea.io";
3
3
  var DEFAULT_TIMEOUT_MS = 3e4;
4
+ var USER_AGENT = `opensea-cli/${"1.0.0"}`;
5
+ var DEFAULT_MAX_RETRIES = 0;
6
+ var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
7
+ function isRetryableStatus(status, method) {
8
+ if (status === 429) return true;
9
+ return status >= 500 && method === "GET";
10
+ }
11
+ function parseRetryAfter(header) {
12
+ if (!header) return void 0;
13
+ const seconds = Number(header);
14
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
15
+ const date = Date.parse(header);
16
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
17
+ return void 0;
18
+ }
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
4
22
  var OpenSeaClient = class {
5
23
  apiKey;
6
24
  baseUrl;
7
25
  defaultChain;
8
26
  timeoutMs;
9
27
  verbose;
28
+ maxRetries;
29
+ retryBaseDelay;
10
30
  constructor(config) {
11
31
  this.apiKey = config.apiKey;
12
32
  this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
13
33
  this.defaultChain = config.chain ?? "ethereum";
14
34
  this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
15
35
  this.verbose = config.verbose ?? false;
36
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
37
+ this.retryBaseDelay = config.retryBaseDelay ?? DEFAULT_RETRY_BASE_DELAY_MS;
38
+ }
39
+ get defaultHeaders() {
40
+ return {
41
+ Accept: "application/json",
42
+ "User-Agent": USER_AGENT,
43
+ "x-api-key": this.apiKey
44
+ };
16
45
  }
17
46
  async get(path, params) {
18
47
  const url = new URL(`${this.baseUrl}${path}`);
@@ -26,21 +55,14 @@ var OpenSeaClient = class {
26
55
  if (this.verbose) {
27
56
  console.error(`[verbose] GET ${url.toString()}`);
28
57
  }
29
- const response = await fetch(url.toString(), {
30
- method: "GET",
31
- headers: {
32
- Accept: "application/json",
33
- "x-api-key": this.apiKey
58
+ const response = await this.fetchWithRetry(
59
+ url.toString(),
60
+ {
61
+ method: "GET",
62
+ headers: this.defaultHeaders
34
63
  },
35
- signal: AbortSignal.timeout(this.timeoutMs)
36
- });
37
- if (this.verbose) {
38
- console.error(`[verbose] ${response.status} ${response.statusText}`);
39
- }
40
- if (!response.ok) {
41
- const body = await response.text();
42
- throw new OpenSeaAPIError(response.status, body, path);
43
- }
64
+ path
65
+ );
44
66
  return response.json();
45
67
  }
46
68
  async post(path, body, params) {
@@ -52,34 +74,67 @@ var OpenSeaClient = class {
52
74
  }
53
75
  }
54
76
  }
55
- const headers = {
56
- Accept: "application/json",
57
- "x-api-key": this.apiKey
58
- };
77
+ const headers = { ...this.defaultHeaders };
59
78
  if (body) {
60
79
  headers["Content-Type"] = "application/json";
61
80
  }
62
81
  if (this.verbose) {
63
82
  console.error(`[verbose] POST ${url.toString()}`);
64
83
  }
65
- const response = await fetch(url.toString(), {
66
- method: "POST",
67
- headers,
68
- body: body ? JSON.stringify(body) : void 0,
69
- signal: AbortSignal.timeout(this.timeoutMs)
70
- });
71
- if (this.verbose) {
72
- console.error(`[verbose] ${response.status} ${response.statusText}`);
73
- }
74
- if (!response.ok) {
75
- const text = await response.text();
76
- throw new OpenSeaAPIError(response.status, text, path);
77
- }
84
+ const response = await this.fetchWithRetry(
85
+ url.toString(),
86
+ {
87
+ method: "POST",
88
+ headers,
89
+ body: body ? JSON.stringify(body) : void 0
90
+ },
91
+ path
92
+ );
78
93
  return response.json();
79
94
  }
80
95
  getDefaultChain() {
81
96
  return this.defaultChain;
82
97
  }
98
+ getApiKeyPrefix() {
99
+ if (this.apiKey.length < 8) return "***";
100
+ return `${this.apiKey.slice(0, 4)}...`;
101
+ }
102
+ async fetchWithRetry(url, init, path) {
103
+ for (let attempt = 0; ; attempt++) {
104
+ const response = await fetch(url, {
105
+ ...init,
106
+ signal: AbortSignal.timeout(this.timeoutMs)
107
+ });
108
+ if (this.verbose) {
109
+ console.error(`[verbose] ${response.status} ${response.statusText}`);
110
+ }
111
+ if (response.ok) {
112
+ return response;
113
+ }
114
+ const method = init.method ?? "GET";
115
+ if (attempt < this.maxRetries && isRetryableStatus(response.status, method)) {
116
+ const retryAfterMs = parseRetryAfter(
117
+ response.headers.get("Retry-After")
118
+ );
119
+ const backoffMs = this.retryBaseDelay * 2 ** attempt;
120
+ const jitterMs = Math.random() * this.retryBaseDelay;
121
+ const delayMs = Math.max(retryAfterMs ?? 0, backoffMs) + jitterMs;
122
+ if (this.verbose) {
123
+ console.error(
124
+ `[verbose] Retry ${attempt + 1}/${this.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
125
+ );
126
+ }
127
+ try {
128
+ await response.body?.cancel();
129
+ } catch {
130
+ }
131
+ await sleep(delayMs);
132
+ continue;
133
+ }
134
+ const text = await response.text();
135
+ throw new OpenSeaAPIError(response.status, text, path);
136
+ }
137
+ }
83
138
  };
84
139
  var OpenSeaAPIError = class extends Error {
85
140
  constructor(statusCode, responseBody, path) {
@@ -89,8 +144,73 @@ var OpenSeaAPIError = class extends Error {
89
144
  this.path = path;
90
145
  this.name = "OpenSeaAPIError";
91
146
  }
147
+ statusCode;
148
+ responseBody;
149
+ path;
92
150
  };
93
151
 
152
+ // src/health.ts
153
+ async function checkHealth(client) {
154
+ const keyPrefix = client.getApiKeyPrefix();
155
+ try {
156
+ await client.get("/api/v2/collections", { limit: 1 });
157
+ } catch (error) {
158
+ let message;
159
+ if (error instanceof OpenSeaAPIError) {
160
+ message = error.statusCode === 429 ? "Rate limited: too many requests" : `API error (${error.statusCode}): ${error.responseBody}`;
161
+ } else {
162
+ message = `Network error: ${error.message}`;
163
+ }
164
+ return {
165
+ status: "error",
166
+ key_prefix: keyPrefix,
167
+ authenticated: false,
168
+ rate_limited: error instanceof OpenSeaAPIError && error.statusCode === 429,
169
+ message
170
+ };
171
+ }
172
+ try {
173
+ await client.get("/api/v2/listings/collection/boredapeyachtclub/all", {
174
+ limit: 1
175
+ });
176
+ return {
177
+ status: "ok",
178
+ key_prefix: keyPrefix,
179
+ authenticated: true,
180
+ rate_limited: false,
181
+ message: "Connectivity and authentication are working"
182
+ };
183
+ } catch (error) {
184
+ if (error instanceof OpenSeaAPIError) {
185
+ if (error.statusCode === 429) {
186
+ return {
187
+ status: "error",
188
+ key_prefix: keyPrefix,
189
+ authenticated: false,
190
+ rate_limited: true,
191
+ message: "Rate limited: too many requests"
192
+ };
193
+ }
194
+ if (error.statusCode === 401 || error.statusCode === 403) {
195
+ return {
196
+ status: "error",
197
+ key_prefix: keyPrefix,
198
+ authenticated: false,
199
+ rate_limited: false,
200
+ message: `Authentication failed (${error.statusCode}): invalid API key`
201
+ };
202
+ }
203
+ }
204
+ return {
205
+ status: "ok",
206
+ key_prefix: keyPrefix,
207
+ authenticated: false,
208
+ rate_limited: false,
209
+ message: "Connectivity is working but authentication could not be verified"
210
+ };
211
+ }
212
+ }
213
+
94
214
  // src/toon.ts
95
215
  var INDENT = " ";
96
216
  var NUMERIC_RE = /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i;
@@ -362,14 +482,21 @@ function formatToon(data) {
362
482
  }
363
483
 
364
484
  // src/output.ts
485
+ var _outputOptions = {};
365
486
  function formatOutput(data, format) {
487
+ const processed = _outputOptions.fields ? filterFields(data, _outputOptions.fields) : data;
488
+ let result;
366
489
  if (format === "table") {
367
- return formatTable(data);
490
+ result = formatTable(processed);
491
+ } else if (format === "toon") {
492
+ result = formatToon(processed);
493
+ } else {
494
+ result = JSON.stringify(processed, null, 2);
368
495
  }
369
- if (format === "toon") {
370
- return formatToon(data);
496
+ if (_outputOptions.maxLines != null) {
497
+ result = truncateOutput(result, _outputOptions.maxLines);
371
498
  }
372
- return JSON.stringify(data, null, 2);
499
+ return result;
373
500
  }
374
501
  function formatTable(data) {
375
502
  if (Array.isArray(data)) {
@@ -405,11 +532,975 @@ function formatTable(data) {
405
532
  }
406
533
  return String(data);
407
534
  }
535
+ function pickFields(obj, fields) {
536
+ const result = {};
537
+ for (const field of fields) {
538
+ if (field in obj) {
539
+ result[field] = obj[field];
540
+ }
541
+ }
542
+ return result;
543
+ }
544
+ function filterFields(data, fields) {
545
+ if (Array.isArray(data)) {
546
+ return data.map((item) => filterFields(item, fields));
547
+ }
548
+ if (data && typeof data === "object") {
549
+ return pickFields(data, fields);
550
+ }
551
+ return data;
552
+ }
553
+ function truncateOutput(text, maxLines) {
554
+ const lines = text.split("\n");
555
+ if (lines.length <= maxLines) return text;
556
+ const omitted = lines.length - maxLines;
557
+ return lines.slice(0, maxLines).join("\n") + `
558
+ ... (${omitted} more line${omitted === 1 ? "" : "s"})`;
559
+ }
560
+
561
+ // src/wallet/fireblocks.generated.ts
562
+ var CHAIN_TO_FIREBLOCKS_ASSET = {
563
+ 1: "ETH",
564
+ 10: "ETH-OPT",
565
+ 130: "UNICHAIN_ETH",
566
+ 137: "MATIC_POLYGON",
567
+ 360: "SHAPE_ETH",
568
+ 1329: "SEI_EVM",
569
+ 1868: "SONEIUM_ETH",
570
+ 2741: "ABSTRACT_ETH",
571
+ 8453: "BASECHAIN_ETH",
572
+ 33139: "APE_CHAIN",
573
+ 42161: "ETH-AETH",
574
+ 43114: "AVAX",
575
+ 80094: "BERA_CHAIN",
576
+ 81457: "BLAST_ETH",
577
+ 7777777: "ZORA_ETH"
578
+ };
579
+
580
+ // src/wallet/fireblocks.ts
581
+ var FIREBLOCKS_API_BASE = "https://api.fireblocks.io";
582
+ var FireblocksAdapter = class _FireblocksAdapter {
583
+ name = "fireblocks";
584
+ onRequest;
585
+ onResponse;
586
+ config;
587
+ cachedAddress;
588
+ constructor(config) {
589
+ this.config = config;
590
+ }
591
+ /**
592
+ * Create a FireblocksAdapter from environment variables.
593
+ * Throws if any required variable is missing.
594
+ */
595
+ static fromEnv() {
596
+ const apiKey = process.env.FIREBLOCKS_API_KEY;
597
+ const apiSecret = process.env.FIREBLOCKS_API_SECRET;
598
+ const vaultId = process.env.FIREBLOCKS_VAULT_ID;
599
+ if (!apiKey) {
600
+ throw new Error("FIREBLOCKS_API_KEY environment variable is required");
601
+ }
602
+ if (!apiSecret) {
603
+ throw new Error("FIREBLOCKS_API_SECRET environment variable is required");
604
+ }
605
+ if (!vaultId) {
606
+ throw new Error("FIREBLOCKS_VAULT_ID environment variable is required");
607
+ }
608
+ return new _FireblocksAdapter({
609
+ apiKey,
610
+ apiSecret,
611
+ vaultId,
612
+ assetId: process.env.FIREBLOCKS_ASSET_ID,
613
+ baseUrl: process.env.FIREBLOCKS_API_BASE_URL
614
+ });
615
+ }
616
+ get baseUrl() {
617
+ return this.config.baseUrl ?? FIREBLOCKS_API_BASE;
618
+ }
619
+ /**
620
+ * Create a JWT for Fireblocks API authentication.
621
+ *
622
+ * Fireblocks uses JWT tokens signed with the API secret (RSA private key).
623
+ * The JWT contains the API key as `sub`, a URI claim for the endpoint path,
624
+ * and a body hash for POST requests.
625
+ *
626
+ * @see https://developers.fireblocks.com/reference/signing-a-request-jwt-structure
627
+ */
628
+ async createJwt(path, bodyHash) {
629
+ const now = Math.floor(Date.now() / 1e3);
630
+ const header = { alg: "RS256", typ: "JWT" };
631
+ const payload = {
632
+ uri: path,
633
+ nonce: crypto.randomUUID(),
634
+ iat: now,
635
+ exp: now + 30,
636
+ sub: this.config.apiKey,
637
+ bodyHash
638
+ };
639
+ const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
640
+ const unsigned = `${b64url(header)}.${b64url(payload)}`;
641
+ const key = await crypto.subtle.importKey(
642
+ "pkcs8",
643
+ this.pemToBuffer(this.config.apiSecret),
644
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
645
+ false,
646
+ ["sign"]
647
+ );
648
+ const sig = await crypto.subtle.sign(
649
+ "RSASSA-PKCS1-v1_5",
650
+ key,
651
+ new TextEncoder().encode(unsigned)
652
+ );
653
+ return `${unsigned}.${Buffer.from(sig).toString("base64url")}`;
654
+ }
655
+ pemToBuffer(pem) {
656
+ const lines = pem.replace(/-----BEGIN .*-----/, "").replace(/-----END .*-----/, "").replace(/\s/g, "");
657
+ const buf = Buffer.from(lines, "base64");
658
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
659
+ }
660
+ async hashBody(body) {
661
+ const hash = await crypto.subtle.digest(
662
+ "SHA-256",
663
+ new TextEncoder().encode(body)
664
+ );
665
+ return Buffer.from(hash).toString("hex");
666
+ }
667
+ resolveAssetId(chainId) {
668
+ if (this.config.assetId) return this.config.assetId;
669
+ const asset = CHAIN_TO_FIREBLOCKS_ASSET[chainId];
670
+ if (!asset) {
671
+ throw new Error(
672
+ `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(", ")}`
673
+ );
674
+ }
675
+ return asset;
676
+ }
677
+ async getAddress() {
678
+ if (this.cachedAddress) return this.cachedAddress;
679
+ const assetId = this.config.assetId ?? "ETH";
680
+ const path = `/v1/vault/accounts/${this.config.vaultId}/${assetId}/addresses`;
681
+ const bodyHash = await this.hashBody("");
682
+ const jwt = await this.createJwt(path, bodyHash);
683
+ const response = await fetch(`${this.baseUrl}${path}`, {
684
+ headers: {
685
+ "X-API-Key": this.config.apiKey,
686
+ Authorization: `Bearer ${jwt}`
687
+ }
688
+ });
689
+ if (!response.ok) {
690
+ const body = await response.text();
691
+ throw new Error(
692
+ `Fireblocks getAddress failed (${response.status}): ${body}`
693
+ );
694
+ }
695
+ const data = await response.json();
696
+ if (!data[0]?.address) {
697
+ throw new Error("Fireblocks returned no addresses for vault");
698
+ }
699
+ this.cachedAddress = data[0].address;
700
+ return data[0].address;
701
+ }
702
+ async sendTransaction(tx) {
703
+ this.onRequest?.("sendTransaction", tx);
704
+ const startTime = Date.now();
705
+ const assetId = this.resolveAssetId(tx.chainId);
706
+ const path = "/v1/transactions";
707
+ const requestBody = {
708
+ assetId,
709
+ operation: "CONTRACT_CALL",
710
+ source: {
711
+ type: "VAULT_ACCOUNT",
712
+ id: this.config.vaultId
713
+ },
714
+ destination: {
715
+ type: "ONE_TIME_ADDRESS",
716
+ oneTimeAddress: { address: tx.to }
717
+ },
718
+ amount: tx.value === "0" ? "0" : tx.value,
719
+ extraParameters: {
720
+ contractCallData: tx.data
721
+ }
722
+ };
723
+ const bodyStr = JSON.stringify(requestBody);
724
+ const bodyHash = await this.hashBody(bodyStr);
725
+ const jwt = await this.createJwt(path, bodyHash);
726
+ const response = await fetch(`${this.baseUrl}${path}`, {
727
+ method: "POST",
728
+ headers: {
729
+ "Content-Type": "application/json",
730
+ "X-API-Key": this.config.apiKey,
731
+ Authorization: `Bearer ${jwt}`
732
+ },
733
+ body: bodyStr
734
+ });
735
+ if (!response.ok) {
736
+ const body = await response.text();
737
+ throw new Error(
738
+ `Fireblocks sendTransaction failed (${response.status}): ${body}`
739
+ );
740
+ }
741
+ const data = await response.json();
742
+ if (data.txHash) {
743
+ const result2 = { hash: data.txHash };
744
+ this.onResponse?.("sendTransaction", result2, Date.now() - startTime);
745
+ return result2;
746
+ }
747
+ const result = await this.waitForTransaction(data.id);
748
+ this.onResponse?.("sendTransaction", result, Date.now() - startTime);
749
+ return result;
750
+ }
751
+ /**
752
+ * Poll a Fireblocks transaction until it reaches a terminal status.
753
+ * Fireblocks MPC signing + broadcast is asynchronous, so the initial
754
+ * POST returns a transaction ID that must be polled for the final hash.
755
+ */
756
+ async waitForTransaction(txId) {
757
+ const maxAttempts = process.env.FIREBLOCKS_MAX_POLL_ATTEMPTS ? Number.parseInt(process.env.FIREBLOCKS_MAX_POLL_ATTEMPTS, 10) : 60;
758
+ const pollIntervalMs = 2e3;
759
+ for (let i = 0; i < maxAttempts; i++) {
760
+ const path = `/v1/transactions/${txId}`;
761
+ const bodyHash = await this.hashBody("");
762
+ const jwt = await this.createJwt(path, bodyHash);
763
+ const response = await fetch(`${this.baseUrl}${path}`, {
764
+ headers: {
765
+ "X-API-Key": this.config.apiKey,
766
+ Authorization: `Bearer ${jwt}`
767
+ }
768
+ });
769
+ if (!response.ok) {
770
+ const body = await response.text();
771
+ throw new Error(`Fireblocks poll failed (${response.status}): ${body}`);
772
+ }
773
+ const data = await response.json();
774
+ if (data.status === "COMPLETED" && data.txHash) {
775
+ return { hash: data.txHash };
776
+ }
777
+ if (data.status === "FAILED" || data.status === "REJECTED" || data.status === "CANCELLED" || data.status === "BLOCKED") {
778
+ throw new Error(
779
+ `Fireblocks transaction ${txId} ended with status: ${data.status}`
780
+ );
781
+ }
782
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
783
+ }
784
+ throw new Error(
785
+ `Fireblocks transaction ${txId} did not complete within ${maxAttempts * pollIntervalMs / 1e3}s`
786
+ );
787
+ }
788
+ };
789
+
790
+ // src/wallet/private-key.ts
791
+ var HOSTED_RPC_PROVIDERS = [
792
+ "infura.io",
793
+ "alchemy.com",
794
+ "quicknode.com",
795
+ "ankr.com",
796
+ "cloudflare-eth.com",
797
+ "pokt.network",
798
+ "blastapi.io",
799
+ "chainnodes.org",
800
+ "drpc.org"
801
+ ];
802
+ var PrivateKeyAdapter = class _PrivateKeyAdapter {
803
+ name = "private-key";
804
+ onRequest;
805
+ onResponse;
806
+ config;
807
+ hasWarned = false;
808
+ constructor(config) {
809
+ this.config = config;
810
+ }
811
+ /**
812
+ * Create a PrivateKeyAdapter from environment variables.
813
+ * Validates the private key format and warns if the RPC URL looks
814
+ * like a hosted provider (which won't support eth_sendTransaction).
815
+ */
816
+ static fromEnv() {
817
+ const privateKey = process.env.PRIVATE_KEY;
818
+ const rpcUrl = process.env.RPC_URL;
819
+ const walletAddress = process.env.WALLET_ADDRESS;
820
+ if (!privateKey) {
821
+ throw new Error("PRIVATE_KEY environment variable is required");
822
+ }
823
+ if (!rpcUrl) {
824
+ throw new Error(
825
+ "RPC_URL environment variable is required when using PRIVATE_KEY"
826
+ );
827
+ }
828
+ if (!walletAddress) {
829
+ throw new Error(
830
+ "WALLET_ADDRESS environment variable is required when using PRIVATE_KEY"
831
+ );
832
+ }
833
+ const cleanKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
834
+ if (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) {
835
+ throw new Error(
836
+ "PRIVATE_KEY must be a 32-byte hex string (64 hex characters, with optional 0x prefix)"
837
+ );
838
+ }
839
+ try {
840
+ const host = new URL(rpcUrl).hostname;
841
+ const isHosted = HOSTED_RPC_PROVIDERS.some(
842
+ (provider) => host.includes(provider)
843
+ );
844
+ if (isHosted) {
845
+ console.warn(
846
+ `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.`
847
+ );
848
+ }
849
+ } catch {
850
+ }
851
+ return new _PrivateKeyAdapter({ privateKey, rpcUrl, walletAddress });
852
+ }
853
+ async getAddress() {
854
+ return this.config.walletAddress;
855
+ }
856
+ async sendTransaction(tx) {
857
+ if (!this.hasWarned) {
858
+ this.hasWarned = true;
859
+ console.warn(
860
+ "WARNING: Using raw PRIVATE_KEY adapter. This is not recommended for production. Use --wallet-provider privy|turnkey|fireblocks for managed wallet security."
861
+ );
862
+ }
863
+ this.onRequest?.("sendTransaction", tx);
864
+ const startTime = Date.now();
865
+ const response = await fetch(this.config.rpcUrl, {
866
+ method: "POST",
867
+ headers: { "Content-Type": "application/json" },
868
+ body: JSON.stringify({
869
+ jsonrpc: "2.0",
870
+ id: 1,
871
+ method: "eth_sendTransaction",
872
+ params: [
873
+ {
874
+ from: this.config.walletAddress,
875
+ to: tx.to,
876
+ data: tx.data,
877
+ value: tx.value === "0" ? "0x0" : `0x${BigInt(tx.value).toString(16)}`,
878
+ chainId: `0x${tx.chainId.toString(16)}`
879
+ }
880
+ ]
881
+ })
882
+ });
883
+ if (!response.ok) {
884
+ const body = await response.text();
885
+ throw new Error(
886
+ `Private key sendTransaction failed (${response.status}): ${body}`
887
+ );
888
+ }
889
+ const data = await response.json();
890
+ if (data.error) {
891
+ throw new Error(
892
+ `Private key sendTransaction RPC error: ${data.error.message}`
893
+ );
894
+ }
895
+ if (!data.result) {
896
+ throw new Error("Private key sendTransaction returned no tx hash");
897
+ }
898
+ const result = { hash: data.result };
899
+ this.onResponse?.("sendTransaction", result, Date.now() - startTime);
900
+ return result;
901
+ }
902
+ };
903
+
904
+ // src/wallet/privy.ts
905
+ var PRIVY_API_BASE = "https://api.privy.io";
906
+ var PrivyAdapter = class _PrivyAdapter {
907
+ name = "privy";
908
+ onRequest;
909
+ onResponse;
910
+ config;
911
+ cachedAddress;
912
+ constructor(config) {
913
+ this.config = config;
914
+ }
915
+ /**
916
+ * Create a PrivyAdapter from environment variables.
917
+ * Throws if any required variable is missing.
918
+ */
919
+ static fromEnv() {
920
+ const appId = process.env.PRIVY_APP_ID;
921
+ const appSecret = process.env.PRIVY_APP_SECRET;
922
+ const walletId = process.env.PRIVY_WALLET_ID;
923
+ if (!appId) {
924
+ throw new Error("PRIVY_APP_ID environment variable is required");
925
+ }
926
+ if (!appSecret) {
927
+ throw new Error("PRIVY_APP_SECRET environment variable is required");
928
+ }
929
+ if (!walletId) {
930
+ throw new Error("PRIVY_WALLET_ID environment variable is required");
931
+ }
932
+ return new _PrivyAdapter({
933
+ appId,
934
+ appSecret,
935
+ walletId,
936
+ baseUrl: process.env.PRIVY_API_BASE_URL
937
+ });
938
+ }
939
+ get baseUrl() {
940
+ return this.config.baseUrl ?? PRIVY_API_BASE;
941
+ }
942
+ get authHeaders() {
943
+ const credentials = Buffer.from(
944
+ `${this.config.appId}:${this.config.appSecret}`
945
+ ).toString("base64");
946
+ return {
947
+ Authorization: `Basic ${credentials}`,
948
+ "privy-app-id": this.config.appId,
949
+ "Content-Type": "application/json"
950
+ };
951
+ }
952
+ async getAddress() {
953
+ if (this.cachedAddress) return this.cachedAddress;
954
+ const response = await fetch(
955
+ `${this.baseUrl}/v1/wallets/${this.config.walletId}`,
956
+ { headers: this.authHeaders }
957
+ );
958
+ if (!response.ok) {
959
+ const body = await response.text();
960
+ throw new Error(`Privy getAddress failed (${response.status}): ${body}`);
961
+ }
962
+ const data = await response.json();
963
+ this.cachedAddress = data.address;
964
+ return data.address;
965
+ }
966
+ async sendTransaction(tx) {
967
+ this.onRequest?.("sendTransaction", tx);
968
+ const startTime = Date.now();
969
+ const caip2 = `eip155:${tx.chainId}`;
970
+ const response = await fetch(
971
+ `${this.baseUrl}/v1/wallets/${this.config.walletId}/rpc`,
972
+ {
973
+ method: "POST",
974
+ headers: this.authHeaders,
975
+ body: JSON.stringify({
976
+ method: "eth_sendTransaction",
977
+ caip2,
978
+ params: {
979
+ transaction: {
980
+ to: tx.to,
981
+ data: tx.data,
982
+ value: tx.value
983
+ }
984
+ }
985
+ })
986
+ }
987
+ );
988
+ if (!response.ok) {
989
+ const body = await response.text();
990
+ throw new Error(
991
+ `Privy sendTransaction failed (${response.status}): ${body}`
992
+ );
993
+ }
994
+ const data = await response.json();
995
+ const result = { hash: data.data.hash };
996
+ this.onResponse?.("sendTransaction", result, Date.now() - startTime);
997
+ return result;
998
+ }
999
+ };
1000
+
1001
+ // src/wallet/turnkey.ts
1002
+ var TURNKEY_API_BASE = "https://api.turnkey.com";
1003
+ var TurnkeyAdapter = class _TurnkeyAdapter {
1004
+ name = "turnkey";
1005
+ onRequest;
1006
+ onResponse;
1007
+ config;
1008
+ constructor(config) {
1009
+ this.config = config;
1010
+ }
1011
+ /**
1012
+ * Create a TurnkeyAdapter from environment variables.
1013
+ * Throws if any required variable is missing.
1014
+ */
1015
+ static fromEnv() {
1016
+ const apiPublicKey = process.env.TURNKEY_API_PUBLIC_KEY;
1017
+ const apiPrivateKey = process.env.TURNKEY_API_PRIVATE_KEY;
1018
+ const organizationId = process.env.TURNKEY_ORGANIZATION_ID;
1019
+ const walletAddress = process.env.TURNKEY_WALLET_ADDRESS;
1020
+ if (!apiPublicKey) {
1021
+ throw new Error("TURNKEY_API_PUBLIC_KEY environment variable is required");
1022
+ }
1023
+ if (!apiPrivateKey) {
1024
+ throw new Error(
1025
+ "TURNKEY_API_PRIVATE_KEY environment variable is required"
1026
+ );
1027
+ }
1028
+ if (!organizationId) {
1029
+ throw new Error(
1030
+ "TURNKEY_ORGANIZATION_ID environment variable is required"
1031
+ );
1032
+ }
1033
+ if (!walletAddress) {
1034
+ throw new Error("TURNKEY_WALLET_ADDRESS environment variable is required");
1035
+ }
1036
+ const rpcUrl = process.env.TURNKEY_RPC_URL;
1037
+ if (!rpcUrl) {
1038
+ throw new Error(
1039
+ "TURNKEY_RPC_URL environment variable is required. It is used for gas estimation and transaction broadcasting."
1040
+ );
1041
+ }
1042
+ return new _TurnkeyAdapter({
1043
+ apiPublicKey,
1044
+ apiPrivateKey,
1045
+ organizationId,
1046
+ walletAddress,
1047
+ rpcUrl,
1048
+ privateKeyId: process.env.TURNKEY_PRIVATE_KEY_ID,
1049
+ baseUrl: process.env.TURNKEY_API_BASE_URL
1050
+ });
1051
+ }
1052
+ get baseUrl() {
1053
+ return this.config.baseUrl ?? TURNKEY_API_BASE;
1054
+ }
1055
+ /**
1056
+ * Sign a Turnkey API request using the API key pair (P-256 ECDSA).
1057
+ *
1058
+ * Turnkey uses a stamp-based authentication scheme: the request body
1059
+ * is hashed with SHA-256 and signed with the P-256 private key. The
1060
+ * stamp JSON (publicKey + scheme + signature) is then base64url-encoded
1061
+ * and sent in the X-Stamp header.
1062
+ *
1063
+ * @see https://docs.turnkey.com/developer-tools/api-overview/stamps
1064
+ */
1065
+ async stamp(body) {
1066
+ const encoder = new TextEncoder();
1067
+ const bodyHash = await crypto.subtle.digest("SHA-256", encoder.encode(body));
1068
+ const keyData = hexToBytes(this.config.apiPrivateKey);
1069
+ const cryptoKey = await crypto.subtle.importKey(
1070
+ "pkcs8",
1071
+ derEncodeP256PrivateKey(keyData),
1072
+ { name: "ECDSA", namedCurve: "P-256" },
1073
+ false,
1074
+ ["sign"]
1075
+ );
1076
+ const p1363Sig = await crypto.subtle.sign(
1077
+ { name: "ECDSA", hash: "SHA-256" },
1078
+ cryptoKey,
1079
+ bodyHash
1080
+ );
1081
+ const derSig = p1363ToDer(new Uint8Array(p1363Sig));
1082
+ const signatureHex = bytesToHex(derSig);
1083
+ const stampJson = JSON.stringify({
1084
+ publicKey: this.config.apiPublicKey,
1085
+ scheme: "SIGNATURE_SCHEME_TK_API_P256",
1086
+ signature: signatureHex
1087
+ });
1088
+ return Buffer.from(stampJson).toString("base64url");
1089
+ }
1090
+ async signedRequest(path, body) {
1091
+ const bodyStr = JSON.stringify(body);
1092
+ const stampValue = await this.stamp(bodyStr);
1093
+ return fetch(`${this.baseUrl}${path}`, {
1094
+ method: "POST",
1095
+ headers: {
1096
+ "Content-Type": "application/json",
1097
+ "X-Stamp": stampValue
1098
+ },
1099
+ body: bodyStr
1100
+ });
1101
+ }
1102
+ async getAddress() {
1103
+ return this.config.walletAddress;
1104
+ }
1105
+ async sendTransaction(tx) {
1106
+ this.onRequest?.("sendTransaction", tx);
1107
+ const startTime = Date.now();
1108
+ const { rpcUrl } = this.config;
1109
+ const gasParams = await this.estimateGasParams(rpcUrl, tx);
1110
+ const rlpHex = rlpEncodeEip1559Tx({
1111
+ chainId: tx.chainId,
1112
+ nonce: gasParams.nonce,
1113
+ maxPriorityFeePerGas: gasParams.maxPriorityFeePerGas,
1114
+ maxFeePerGas: gasParams.maxFeePerGas,
1115
+ gasLimit: gasParams.gasLimit,
1116
+ to: tx.to,
1117
+ data: tx.data,
1118
+ value: tx.value
1119
+ });
1120
+ const signWith = this.config.privateKeyId ?? this.config.walletAddress;
1121
+ const response = await this.signedRequest(
1122
+ "/public/v1/submit/sign_transaction",
1123
+ {
1124
+ type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
1125
+ organizationId: this.config.organizationId,
1126
+ timestampMs: Date.now().toString(),
1127
+ parameters: {
1128
+ signWith,
1129
+ type: "TRANSACTION_TYPE_ETHEREUM",
1130
+ unsignedTransaction: rlpHex
1131
+ }
1132
+ }
1133
+ );
1134
+ if (!response.ok) {
1135
+ const body = await response.text();
1136
+ throw new Error(
1137
+ `Turnkey sendTransaction failed (${response.status}): ${body}`
1138
+ );
1139
+ }
1140
+ const data = await response.json();
1141
+ const signedTx = data.activity.result?.signTransactionResult?.signedTransaction;
1142
+ if (!signedTx) {
1143
+ throw new Error(
1144
+ `Turnkey sign transaction did not return a signed payload (activity status: ${data.activity.status})`
1145
+ );
1146
+ }
1147
+ const rpcResponse = await fetch(rpcUrl, {
1148
+ method: "POST",
1149
+ headers: { "Content-Type": "application/json" },
1150
+ body: JSON.stringify({
1151
+ jsonrpc: "2.0",
1152
+ id: 1,
1153
+ method: "eth_sendRawTransaction",
1154
+ params: [signedTx]
1155
+ })
1156
+ });
1157
+ if (!rpcResponse.ok) {
1158
+ const rpcBody = await rpcResponse.text();
1159
+ throw new Error(
1160
+ `Turnkey broadcast failed (${rpcResponse.status}): ${rpcBody}`
1161
+ );
1162
+ }
1163
+ const rpcData = await rpcResponse.json();
1164
+ if (rpcData.error) {
1165
+ throw new Error(`Turnkey broadcast RPC error: ${rpcData.error.message}`);
1166
+ }
1167
+ if (!rpcData.result) {
1168
+ throw new Error("Turnkey broadcast returned no tx hash");
1169
+ }
1170
+ const result = { hash: rpcData.result };
1171
+ this.onResponse?.("sendTransaction", result, Date.now() - startTime);
1172
+ return result;
1173
+ }
1174
+ /**
1175
+ * Populate gas parameters via JSON-RPC calls to the target chain.
1176
+ * Mirrors what ethers.js provider.populateTransaction() does internally.
1177
+ *
1178
+ * Makes three parallel RPC calls:
1179
+ * - eth_getTransactionCount (nonce)
1180
+ * - eth_estimateGas (gasLimit)
1181
+ * - eth_maxPriorityFeePerGas + eth_getBlockByNumber (fee data)
1182
+ */
1183
+ async estimateGasParams(rpcUrl, tx) {
1184
+ const from = this.config.walletAddress;
1185
+ const txValue = tx.value === "0" ? "0x0" : `0x${BigInt(tx.value).toString(16)}`;
1186
+ const [nonceResult, gasEstimateResult, feeDataResult] = await Promise.all([
1187
+ this.rpcCall(rpcUrl, "eth_getTransactionCount", [from, "pending"]),
1188
+ this.rpcCall(rpcUrl, "eth_estimateGas", [
1189
+ {
1190
+ from,
1191
+ to: tx.to,
1192
+ data: tx.data || "0x",
1193
+ value: txValue
1194
+ }
1195
+ ]),
1196
+ this.rpcCall(rpcUrl, "eth_feeHistory", [1, "latest", [50]])
1197
+ ]);
1198
+ const nonce = BigInt(nonceResult);
1199
+ const rawGasLimit = BigInt(gasEstimateResult);
1200
+ const gasLimit = rawGasLimit * 120n / 100n;
1201
+ const feeHistory = feeDataResult;
1202
+ const latestBaseFee = BigInt(
1203
+ feeHistory.baseFeePerGas[1] ?? feeHistory.baseFeePerGas[0]
1204
+ );
1205
+ const maxPriorityFeePerGas = feeHistory.reward?.[0]?.[0] ? BigInt(feeHistory.reward[0][0]) : 1500000000n;
1206
+ const maxFeePerGas = latestBaseFee * 2n + maxPriorityFeePerGas;
1207
+ return { nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas };
1208
+ }
1209
+ /** Make a single JSON-RPC call */
1210
+ async rpcCall(rpcUrl, method, params) {
1211
+ const response = await fetch(rpcUrl, {
1212
+ method: "POST",
1213
+ headers: { "Content-Type": "application/json" },
1214
+ body: JSON.stringify({
1215
+ jsonrpc: "2.0",
1216
+ id: 1,
1217
+ method,
1218
+ params
1219
+ })
1220
+ });
1221
+ if (!response.ok) {
1222
+ const body = await response.text();
1223
+ throw new Error(
1224
+ `Turnkey RPC ${method} failed (${response.status}): ${body}`
1225
+ );
1226
+ }
1227
+ const data = await response.json();
1228
+ if (data.error) {
1229
+ throw new Error(`Turnkey RPC ${method} error: ${data.error.message}`);
1230
+ }
1231
+ return data.result;
1232
+ }
1233
+ };
1234
+ function rlpEncodeEip1559Tx(tx) {
1235
+ const chainIdBytes = bigIntToBytes(BigInt(tx.chainId));
1236
+ const nonce = bigIntToBytes(tx.nonce);
1237
+ const maxPriorityFeePerGas = bigIntToBytes(tx.maxPriorityFeePerGas);
1238
+ const maxFeePerGas = bigIntToBytes(tx.maxFeePerGas);
1239
+ const gasLimit = bigIntToBytes(tx.gasLimit);
1240
+ const toBytes = hexToBytes(tx.to);
1241
+ const valueBytes = tx.value === "0" ? new Uint8Array(0) : bigIntToBytes(BigInt(tx.value));
1242
+ const dataBytes = tx.data ? hexToBytes(tx.data) : new Uint8Array(0);
1243
+ const fields = [
1244
+ rlpEncodeBytes(chainIdBytes),
1245
+ rlpEncodeBytes(nonce),
1246
+ rlpEncodeBytes(maxPriorityFeePerGas),
1247
+ rlpEncodeBytes(maxFeePerGas),
1248
+ rlpEncodeBytes(gasLimit),
1249
+ rlpEncodeBytes(toBytes),
1250
+ rlpEncodeBytes(valueBytes),
1251
+ rlpEncodeBytes(dataBytes),
1252
+ rlpEncodeList([])
1253
+ // empty access list
1254
+ ];
1255
+ const rlpList = rlpEncodeList(fields);
1256
+ const result = new Uint8Array(1 + rlpList.length);
1257
+ result[0] = 2;
1258
+ result.set(rlpList, 1);
1259
+ return bytesToHex(result);
1260
+ }
1261
+ function rlpEncodeBytes(bytes) {
1262
+ if (bytes.length === 1 && bytes[0] < 128) {
1263
+ return bytes;
1264
+ }
1265
+ if (bytes.length === 0) {
1266
+ return new Uint8Array([128]);
1267
+ }
1268
+ if (bytes.length <= 55) {
1269
+ const result2 = new Uint8Array(1 + bytes.length);
1270
+ result2[0] = 128 + bytes.length;
1271
+ result2.set(bytes, 1);
1272
+ return result2;
1273
+ }
1274
+ const lenBytes = bigIntToBytes(BigInt(bytes.length));
1275
+ const result = new Uint8Array(1 + lenBytes.length + bytes.length);
1276
+ result[0] = 183 + lenBytes.length;
1277
+ result.set(lenBytes, 1);
1278
+ result.set(bytes, 1 + lenBytes.length);
1279
+ return result;
1280
+ }
1281
+ function rlpEncodeList(items) {
1282
+ let totalLen = 0;
1283
+ for (const item of items) totalLen += item.length;
1284
+ if (totalLen <= 55) {
1285
+ const result2 = new Uint8Array(1 + totalLen);
1286
+ result2[0] = 192 + totalLen;
1287
+ let offset2 = 1;
1288
+ for (const item of items) {
1289
+ result2.set(item, offset2);
1290
+ offset2 += item.length;
1291
+ }
1292
+ return result2;
1293
+ }
1294
+ const lenBytes = bigIntToBytes(BigInt(totalLen));
1295
+ const result = new Uint8Array(1 + lenBytes.length + totalLen);
1296
+ result[0] = 247 + lenBytes.length;
1297
+ result.set(lenBytes, 1);
1298
+ let offset = 1 + lenBytes.length;
1299
+ for (const item of items) {
1300
+ result.set(item, offset);
1301
+ offset += item.length;
1302
+ }
1303
+ return result;
1304
+ }
1305
+ function bigIntToBytes(value) {
1306
+ if (value === 0n) return new Uint8Array(0);
1307
+ const hex = value.toString(16);
1308
+ const padded = hex.length % 2 === 0 ? hex : `0${hex}`;
1309
+ return hexToBytes(padded);
1310
+ }
1311
+ function hexToBytes(hex) {
1312
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1313
+ const bytes = new Uint8Array(clean.length / 2);
1314
+ for (let i = 0; i < clean.length; i += 2) {
1315
+ bytes[i / 2] = Number.parseInt(clean.slice(i, i + 2), 16);
1316
+ }
1317
+ return bytes;
1318
+ }
1319
+ function bytesToHex(bytes) {
1320
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1321
+ }
1322
+ function p1363ToDer(p1363) {
1323
+ const r = p1363.subarray(0, 32);
1324
+ const s = p1363.subarray(32, 64);
1325
+ const rDer = integerToDer(r);
1326
+ const sDer = integerToDer(s);
1327
+ const seqLen = rDer.length + sDer.length;
1328
+ const result = new Uint8Array(2 + seqLen);
1329
+ result[0] = 48;
1330
+ result[1] = seqLen;
1331
+ result.set(rDer, 2);
1332
+ result.set(sDer, 2 + rDer.length);
1333
+ return result;
1334
+ }
1335
+ function integerToDer(bytes) {
1336
+ let start = 0;
1337
+ while (start < bytes.length - 1 && bytes[start] === 0) start++;
1338
+ const stripped = bytes.subarray(start);
1339
+ const needsPad = stripped[0] >= 128;
1340
+ const len = stripped.length + (needsPad ? 1 : 0);
1341
+ const result = new Uint8Array(2 + len);
1342
+ result[0] = 2;
1343
+ result[1] = len;
1344
+ if (needsPad) {
1345
+ result[2] = 0;
1346
+ result.set(stripped, 3);
1347
+ } else {
1348
+ result.set(stripped, 2);
1349
+ }
1350
+ return result;
1351
+ }
1352
+ function derEncodeP256PrivateKey(rawKey) {
1353
+ const header = new Uint8Array([
1354
+ 48,
1355
+ 65,
1356
+ // SEQUENCE (65 bytes)
1357
+ 2,
1358
+ 1,
1359
+ 0,
1360
+ // INTEGER 0 (version)
1361
+ 48,
1362
+ 19,
1363
+ // SEQUENCE (19 bytes) - AlgorithmIdentifier
1364
+ 6,
1365
+ 7,
1366
+ // OID (7 bytes) - id-ecPublicKey
1367
+ 42,
1368
+ 134,
1369
+ 72,
1370
+ 206,
1371
+ 61,
1372
+ 2,
1373
+ 1,
1374
+ 6,
1375
+ 8,
1376
+ // OID (8 bytes) - secp256r1
1377
+ 42,
1378
+ 134,
1379
+ 72,
1380
+ 206,
1381
+ 61,
1382
+ 3,
1383
+ 1,
1384
+ 7,
1385
+ 4,
1386
+ 39,
1387
+ // OCTET STRING (39 bytes)
1388
+ 48,
1389
+ 37,
1390
+ // SEQUENCE (37 bytes)
1391
+ 2,
1392
+ 1,
1393
+ 1,
1394
+ // INTEGER 1 (version)
1395
+ 4,
1396
+ 32
1397
+ // OCTET STRING (32 bytes) - private key
1398
+ ]);
1399
+ const result = new Uint8Array(header.length + rawKey.length);
1400
+ result.set(header);
1401
+ result.set(rawKey, header.length);
1402
+ return result.buffer;
1403
+ }
1404
+
1405
+ // src/wallet/chains.generated.ts
1406
+ var CHAIN_IDS = {
1407
+ ethereum: 1,
1408
+ mainnet: 1,
1409
+ optimism: 10,
1410
+ unichain: 130,
1411
+ polygon: 137,
1412
+ matic: 137,
1413
+ monad: 143,
1414
+ shape: 360,
1415
+ flow: 747,
1416
+ hyperevm: 999,
1417
+ sei: 1329,
1418
+ soneium: 1868,
1419
+ ronin: 2020,
1420
+ abstract: 2741,
1421
+ megaeth: 4326,
1422
+ somnia: 5031,
1423
+ b3: 8333,
1424
+ base: 8453,
1425
+ ape_chain: 33139,
1426
+ apechain: 33139,
1427
+ arbitrum: 42161,
1428
+ avalanche: 43114,
1429
+ gunzilla: 43419,
1430
+ ink: 57073,
1431
+ animechain: 69e3,
1432
+ bera_chain: 80094,
1433
+ berachain: 80094,
1434
+ blast: 81457,
1435
+ zora: 7777777
1436
+ };
1437
+ function resolveChainId(chain) {
1438
+ if (typeof chain === "number") return chain;
1439
+ const asNum = Number(chain);
1440
+ if (!Number.isNaN(asNum) && Number.isInteger(asNum)) return asNum;
1441
+ const id = CHAIN_IDS[chain];
1442
+ if (id === void 0) {
1443
+ throw new Error(
1444
+ `Unknown chain "${chain}". Pass a numeric chain ID or use a known name: ${Object.keys(CHAIN_IDS).join(", ")}`
1445
+ );
1446
+ }
1447
+ return id;
1448
+ }
1449
+
1450
+ // src/wallet/index.ts
1451
+ var WALLET_PROVIDERS = [
1452
+ "privy",
1453
+ "turnkey",
1454
+ "fireblocks",
1455
+ "private-key"
1456
+ ];
1457
+ function createWalletFromEnv(provider) {
1458
+ if (provider) {
1459
+ return createAdapter(provider);
1460
+ }
1461
+ const hasTurnkey = !!process.env.TURNKEY_API_PUBLIC_KEY && !!process.env.TURNKEY_ORGANIZATION_ID;
1462
+ const hasFireblocks = !!process.env.FIREBLOCKS_API_KEY && !!process.env.FIREBLOCKS_VAULT_ID;
1463
+ const hasPrivateKey = !!process.env.PRIVATE_KEY && !!process.env.RPC_URL;
1464
+ const hasPrivy = !!process.env.PRIVY_APP_ID && !!process.env.PRIVY_APP_SECRET;
1465
+ const detected = [
1466
+ hasTurnkey && "turnkey",
1467
+ hasFireblocks && "fireblocks",
1468
+ hasPrivateKey && "private-key",
1469
+ hasPrivy && "privy"
1470
+ ].filter(Boolean);
1471
+ if (detected.length > 1) {
1472
+ console.warn(
1473
+ `WARNING: Multiple wallet providers detected: ${detected.join(", ")}. Using ${detected[0]}. Set --wallet-provider explicitly to avoid ambiguity.`
1474
+ );
1475
+ }
1476
+ if (hasTurnkey) return TurnkeyAdapter.fromEnv();
1477
+ if (hasFireblocks) return FireblocksAdapter.fromEnv();
1478
+ if (hasPrivateKey) return PrivateKeyAdapter.fromEnv();
1479
+ return PrivyAdapter.fromEnv();
1480
+ }
1481
+ function createAdapter(provider) {
1482
+ switch (provider) {
1483
+ case "privy":
1484
+ return PrivyAdapter.fromEnv();
1485
+ case "turnkey":
1486
+ return TurnkeyAdapter.fromEnv();
1487
+ case "fireblocks":
1488
+ return FireblocksAdapter.fromEnv();
1489
+ case "private-key":
1490
+ return PrivateKeyAdapter.fromEnv();
1491
+ default:
1492
+ throw new Error(
1493
+ `Unknown wallet provider "${provider}". Valid providers: ${WALLET_PROVIDERS.join(", ")}`
1494
+ );
1495
+ }
1496
+ }
408
1497
 
409
1498
  // src/sdk.ts
410
1499
  var OpenSeaCLI = class {
411
1500
  client;
1501
+ chains;
412
1502
  collections;
1503
+ drops;
413
1504
  nfts;
414
1505
  listings;
415
1506
  offers;
@@ -418,9 +1509,12 @@ var OpenSeaCLI = class {
418
1509
  tokens;
419
1510
  search;
420
1511
  swaps;
1512
+ health;
421
1513
  constructor(config) {
422
1514
  this.client = new OpenSeaClient(config);
1515
+ this.chains = new ChainsAPI(this.client);
423
1516
  this.collections = new CollectionsAPI(this.client);
1517
+ this.drops = new DropsAPI(this.client);
424
1518
  this.nfts = new NFTsAPI(this.client);
425
1519
  this.listings = new ListingsAPI(this.client);
426
1520
  this.offers = new OffersAPI(this.client);
@@ -429,12 +1523,23 @@ var OpenSeaCLI = class {
429
1523
  this.tokens = new TokensAPI(this.client);
430
1524
  this.search = new SearchAPI(this.client);
431
1525
  this.swaps = new SwapsAPI(this.client);
1526
+ this.health = new HealthAPI(this.client);
1527
+ }
1528
+ };
1529
+ var ChainsAPI = class {
1530
+ constructor(client) {
1531
+ this.client = client;
1532
+ }
1533
+ client;
1534
+ async list() {
1535
+ return this.client.get("/api/v2/chains");
432
1536
  }
433
1537
  };
434
1538
  var CollectionsAPI = class {
435
1539
  constructor(client) {
436
1540
  this.client = client;
437
1541
  }
1542
+ client;
438
1543
  async get(slug) {
439
1544
  return this.client.get(`/api/v2/collections/${slug}`);
440
1545
  }
@@ -454,11 +1559,53 @@ var CollectionsAPI = class {
454
1559
  async traits(slug) {
455
1560
  return this.client.get(`/api/v2/traits/${slug}`);
456
1561
  }
1562
+ async trending(options) {
1563
+ return this.client.get("/api/v2/collections/trending", {
1564
+ timeframe: options?.timeframe,
1565
+ chains: options?.chains?.join(","),
1566
+ category: options?.category,
1567
+ limit: options?.limit,
1568
+ cursor: options?.next
1569
+ });
1570
+ }
1571
+ async top(options) {
1572
+ return this.client.get("/api/v2/collections/top", {
1573
+ sort_by: options?.sortBy,
1574
+ chains: options?.chains?.join(","),
1575
+ category: options?.category,
1576
+ limit: options?.limit,
1577
+ cursor: options?.next
1578
+ });
1579
+ }
1580
+ };
1581
+ var DropsAPI = class {
1582
+ constructor(client) {
1583
+ this.client = client;
1584
+ }
1585
+ client;
1586
+ async list(options) {
1587
+ return this.client.get("/api/v2/drops", {
1588
+ type: options?.type,
1589
+ chains: options?.chains?.join(","),
1590
+ limit: options?.limit,
1591
+ cursor: options?.next
1592
+ });
1593
+ }
1594
+ async get(slug) {
1595
+ return this.client.get(`/api/v2/drops/${slug}`);
1596
+ }
1597
+ async mint(slug, options) {
1598
+ return this.client.post(`/api/v2/drops/${slug}/mint`, {
1599
+ minter: options.minter,
1600
+ quantity: options.quantity ?? 1
1601
+ });
1602
+ }
457
1603
  };
458
1604
  var NFTsAPI = class {
459
1605
  constructor(client) {
460
1606
  this.client = client;
461
1607
  }
1608
+ client;
462
1609
  async get(chain, address, identifier) {
463
1610
  return this.client.get(
464
1611
  `/api/v2/chain/${chain}/contract/${address}/nfts/${identifier}`
@@ -490,11 +1637,23 @@ var NFTsAPI = class {
490
1637
  async getContract(chain, address) {
491
1638
  return this.client.get(`/api/v2/chain/${chain}/contract/${address}`);
492
1639
  }
1640
+ async validateMetadata(chain, address, identifier, options) {
1641
+ const params = {};
1642
+ if (options?.ignoreCachedItemUrls) {
1643
+ params.ignoreCachedItemUrls = true;
1644
+ }
1645
+ return this.client.post(
1646
+ `/api/v2/chain/${chain}/contract/${address}/nfts/${identifier}/validate-metadata`,
1647
+ void 0,
1648
+ params
1649
+ );
1650
+ }
493
1651
  };
494
1652
  var ListingsAPI = class {
495
1653
  constructor(client) {
496
1654
  this.client = client;
497
1655
  }
1656
+ client;
498
1657
  async all(collectionSlug, options) {
499
1658
  return this.client.get(
500
1659
  `/api/v2/listings/collection/${collectionSlug}/all`,
@@ -517,6 +1676,7 @@ var OffersAPI = class {
517
1676
  constructor(client) {
518
1677
  this.client = client;
519
1678
  }
1679
+ client;
520
1680
  async all(collectionSlug, options) {
521
1681
  return this.client.get(`/api/v2/offers/collection/${collectionSlug}/all`, {
522
1682
  limit: options?.limit,
@@ -550,6 +1710,7 @@ var EventsAPI = class {
550
1710
  constructor(client) {
551
1711
  this.client = client;
552
1712
  }
1713
+ client;
553
1714
  async list(options) {
554
1715
  return this.client.get("/api/v2/events", {
555
1716
  event_type: options?.eventType,
@@ -590,14 +1751,29 @@ var AccountsAPI = class {
590
1751
  constructor(client) {
591
1752
  this.client = client;
592
1753
  }
1754
+ client;
593
1755
  async get(address) {
594
1756
  return this.client.get(`/api/v2/accounts/${address}`);
595
1757
  }
1758
+ async tokens(address, options) {
1759
+ return this.client.get(`/api/v2/account/${address}/tokens`, {
1760
+ chains: options?.chains?.join(","),
1761
+ limit: options?.limit,
1762
+ sort_by: options?.sortBy,
1763
+ sort_direction: options?.sortDirection,
1764
+ disable_spam_filtering: options?.disableSpamFiltering,
1765
+ cursor: options?.next
1766
+ });
1767
+ }
1768
+ async resolve(identifier) {
1769
+ return this.client.get(`/api/v2/accounts/resolve/${identifier}`);
1770
+ }
596
1771
  };
597
1772
  var TokensAPI = class {
598
1773
  constructor(client) {
599
1774
  this.client = client;
600
1775
  }
1776
+ client;
601
1777
  async trending(options) {
602
1778
  return this.client.get("/api/v2/tokens/trending", {
603
1779
  limit: options?.limit,
@@ -620,6 +1796,7 @@ var SearchAPI = class {
620
1796
  constructor(client) {
621
1797
  this.client = client;
622
1798
  }
1799
+ client;
623
1800
  async query(query, options) {
624
1801
  return this.client.get("/api/v2/search", {
625
1802
  query,
@@ -633,6 +1810,7 @@ var SwapsAPI = class {
633
1810
  constructor(client) {
634
1811
  this.client = client;
635
1812
  }
1813
+ client;
636
1814
  async quote(options) {
637
1815
  return this.client.get("/api/v2/swap/quote", {
638
1816
  from_chain: options.fromChain,
@@ -645,12 +1823,74 @@ var SwapsAPI = class {
645
1823
  recipient: options.recipient
646
1824
  });
647
1825
  }
1826
+ /**
1827
+ * Get a swap quote and execute all transactions using the provided wallet adapter.
1828
+ * Returns an array of transaction results (one per transaction in the quote).
1829
+ *
1830
+ * @param options - Swap parameters (chains, addresses, quantity, etc.)
1831
+ * @param wallet - Wallet adapter to sign and send transactions
1832
+ * @param callbacks - Optional callbacks for progress reporting and skipped txs
1833
+ */
1834
+ async execute(options, wallet, callbacks) {
1835
+ const address = options.address ?? await wallet.getAddress();
1836
+ const quote = await this.quote({ ...options, address });
1837
+ callbacks?.onQuote?.(quote);
1838
+ if (!quote.transactions || quote.transactions.length === 0) {
1839
+ throw new Error(
1840
+ "Swap quote returned zero transactions \u2014 the swap may not be available for these tokens/chains."
1841
+ );
1842
+ }
1843
+ const results = [];
1844
+ for (const tx of quote.transactions) {
1845
+ if (!tx.to) {
1846
+ callbacks?.onSkipped?.({
1847
+ chain: tx.chain,
1848
+ reason: "missing 'to' address"
1849
+ });
1850
+ continue;
1851
+ }
1852
+ const chainId = resolveChainId(tx.chain);
1853
+ callbacks?.onSending?.({ to: tx.to, chain: tx.chain, chainId });
1854
+ const result = await wallet.sendTransaction({
1855
+ to: tx.to,
1856
+ data: tx.data,
1857
+ value: tx.value ?? "0",
1858
+ chainId
1859
+ });
1860
+ results.push(result);
1861
+ }
1862
+ if (results.length === 0) {
1863
+ throw new Error(
1864
+ "All swap transactions were skipped (no valid 'to' addresses). The quote may be malformed."
1865
+ );
1866
+ }
1867
+ return results;
1868
+ }
1869
+ };
1870
+ var HealthAPI = class {
1871
+ constructor(client) {
1872
+ this.client = client;
1873
+ }
1874
+ client;
1875
+ async check() {
1876
+ return checkHealth(this.client);
1877
+ }
648
1878
  };
649
1879
  export {
1880
+ CHAIN_IDS,
1881
+ FireblocksAdapter,
650
1882
  OpenSeaAPIError,
651
1883
  OpenSeaCLI,
652
1884
  OpenSeaClient,
1885
+ PrivateKeyAdapter,
1886
+ PrivyAdapter,
1887
+ SwapsAPI,
1888
+ TurnkeyAdapter,
1889
+ WALLET_PROVIDERS,
1890
+ checkHealth,
1891
+ createWalletFromEnv,
653
1892
  formatOutput,
654
- formatToon
1893
+ formatToon,
1894
+ resolveChainId
655
1895
  };
656
1896
  //# sourceMappingURL=index.js.map