@opensea/cli 0.4.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli.js +1254 -35
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +486 -183
- package/dist/index.js +1082 -2
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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/${"0.
|
|
4
|
+
var USER_AGENT = `opensea-cli/${"1.0.0"}`;
|
|
5
5
|
var DEFAULT_MAX_RETRIES = 0;
|
|
6
6
|
var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
|
|
7
7
|
function isRetryableStatus(status, method) {
|
|
@@ -144,6 +144,9 @@ var OpenSeaAPIError = class extends Error {
|
|
|
144
144
|
this.path = path;
|
|
145
145
|
this.name = "OpenSeaAPIError";
|
|
146
146
|
}
|
|
147
|
+
statusCode;
|
|
148
|
+
responseBody;
|
|
149
|
+
path;
|
|
147
150
|
};
|
|
148
151
|
|
|
149
152
|
// src/health.ts
|
|
@@ -555,10 +558,949 @@ function truncateOutput(text, maxLines) {
|
|
|
555
558
|
... (${omitted} more line${omitted === 1 ? "" : "s"})`;
|
|
556
559
|
}
|
|
557
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
|
+
}
|
|
1497
|
+
|
|
558
1498
|
// src/sdk.ts
|
|
559
1499
|
var OpenSeaCLI = class {
|
|
560
1500
|
client;
|
|
1501
|
+
chains;
|
|
561
1502
|
collections;
|
|
1503
|
+
drops;
|
|
562
1504
|
nfts;
|
|
563
1505
|
listings;
|
|
564
1506
|
offers;
|
|
@@ -570,7 +1512,9 @@ var OpenSeaCLI = class {
|
|
|
570
1512
|
health;
|
|
571
1513
|
constructor(config) {
|
|
572
1514
|
this.client = new OpenSeaClient(config);
|
|
1515
|
+
this.chains = new ChainsAPI(this.client);
|
|
573
1516
|
this.collections = new CollectionsAPI(this.client);
|
|
1517
|
+
this.drops = new DropsAPI(this.client);
|
|
574
1518
|
this.nfts = new NFTsAPI(this.client);
|
|
575
1519
|
this.listings = new ListingsAPI(this.client);
|
|
576
1520
|
this.offers = new OffersAPI(this.client);
|
|
@@ -582,10 +1526,20 @@ var OpenSeaCLI = class {
|
|
|
582
1526
|
this.health = new HealthAPI(this.client);
|
|
583
1527
|
}
|
|
584
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");
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
585
1538
|
var CollectionsAPI = class {
|
|
586
1539
|
constructor(client) {
|
|
587
1540
|
this.client = client;
|
|
588
1541
|
}
|
|
1542
|
+
client;
|
|
589
1543
|
async get(slug) {
|
|
590
1544
|
return this.client.get(`/api/v2/collections/${slug}`);
|
|
591
1545
|
}
|
|
@@ -605,11 +1559,53 @@ var CollectionsAPI = class {
|
|
|
605
1559
|
async traits(slug) {
|
|
606
1560
|
return this.client.get(`/api/v2/traits/${slug}`);
|
|
607
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
|
+
}
|
|
608
1603
|
};
|
|
609
1604
|
var NFTsAPI = class {
|
|
610
1605
|
constructor(client) {
|
|
611
1606
|
this.client = client;
|
|
612
1607
|
}
|
|
1608
|
+
client;
|
|
613
1609
|
async get(chain, address, identifier) {
|
|
614
1610
|
return this.client.get(
|
|
615
1611
|
`/api/v2/chain/${chain}/contract/${address}/nfts/${identifier}`
|
|
@@ -641,11 +1637,23 @@ var NFTsAPI = class {
|
|
|
641
1637
|
async getContract(chain, address) {
|
|
642
1638
|
return this.client.get(`/api/v2/chain/${chain}/contract/${address}`);
|
|
643
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
|
+
}
|
|
644
1651
|
};
|
|
645
1652
|
var ListingsAPI = class {
|
|
646
1653
|
constructor(client) {
|
|
647
1654
|
this.client = client;
|
|
648
1655
|
}
|
|
1656
|
+
client;
|
|
649
1657
|
async all(collectionSlug, options) {
|
|
650
1658
|
return this.client.get(
|
|
651
1659
|
`/api/v2/listings/collection/${collectionSlug}/all`,
|
|
@@ -668,6 +1676,7 @@ var OffersAPI = class {
|
|
|
668
1676
|
constructor(client) {
|
|
669
1677
|
this.client = client;
|
|
670
1678
|
}
|
|
1679
|
+
client;
|
|
671
1680
|
async all(collectionSlug, options) {
|
|
672
1681
|
return this.client.get(`/api/v2/offers/collection/${collectionSlug}/all`, {
|
|
673
1682
|
limit: options?.limit,
|
|
@@ -701,6 +1710,7 @@ var EventsAPI = class {
|
|
|
701
1710
|
constructor(client) {
|
|
702
1711
|
this.client = client;
|
|
703
1712
|
}
|
|
1713
|
+
client;
|
|
704
1714
|
async list(options) {
|
|
705
1715
|
return this.client.get("/api/v2/events", {
|
|
706
1716
|
event_type: options?.eventType,
|
|
@@ -741,14 +1751,29 @@ var AccountsAPI = class {
|
|
|
741
1751
|
constructor(client) {
|
|
742
1752
|
this.client = client;
|
|
743
1753
|
}
|
|
1754
|
+
client;
|
|
744
1755
|
async get(address) {
|
|
745
1756
|
return this.client.get(`/api/v2/accounts/${address}`);
|
|
746
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
|
+
}
|
|
747
1771
|
};
|
|
748
1772
|
var TokensAPI = class {
|
|
749
1773
|
constructor(client) {
|
|
750
1774
|
this.client = client;
|
|
751
1775
|
}
|
|
1776
|
+
client;
|
|
752
1777
|
async trending(options) {
|
|
753
1778
|
return this.client.get("/api/v2/tokens/trending", {
|
|
754
1779
|
limit: options?.limit,
|
|
@@ -771,6 +1796,7 @@ var SearchAPI = class {
|
|
|
771
1796
|
constructor(client) {
|
|
772
1797
|
this.client = client;
|
|
773
1798
|
}
|
|
1799
|
+
client;
|
|
774
1800
|
async query(query, options) {
|
|
775
1801
|
return this.client.get("/api/v2/search", {
|
|
776
1802
|
query,
|
|
@@ -784,6 +1810,7 @@ var SwapsAPI = class {
|
|
|
784
1810
|
constructor(client) {
|
|
785
1811
|
this.client = client;
|
|
786
1812
|
}
|
|
1813
|
+
client;
|
|
787
1814
|
async quote(options) {
|
|
788
1815
|
return this.client.get("/api/v2/swap/quote", {
|
|
789
1816
|
from_chain: options.fromChain,
|
|
@@ -796,21 +1823,74 @@ var SwapsAPI = class {
|
|
|
796
1823
|
recipient: options.recipient
|
|
797
1824
|
});
|
|
798
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
|
+
}
|
|
799
1869
|
};
|
|
800
1870
|
var HealthAPI = class {
|
|
801
1871
|
constructor(client) {
|
|
802
1872
|
this.client = client;
|
|
803
1873
|
}
|
|
1874
|
+
client;
|
|
804
1875
|
async check() {
|
|
805
1876
|
return checkHealth(this.client);
|
|
806
1877
|
}
|
|
807
1878
|
};
|
|
808
1879
|
export {
|
|
1880
|
+
CHAIN_IDS,
|
|
1881
|
+
FireblocksAdapter,
|
|
809
1882
|
OpenSeaAPIError,
|
|
810
1883
|
OpenSeaCLI,
|
|
811
1884
|
OpenSeaClient,
|
|
1885
|
+
PrivateKeyAdapter,
|
|
1886
|
+
PrivyAdapter,
|
|
1887
|
+
SwapsAPI,
|
|
1888
|
+
TurnkeyAdapter,
|
|
1889
|
+
WALLET_PROVIDERS,
|
|
812
1890
|
checkHealth,
|
|
1891
|
+
createWalletFromEnv,
|
|
813
1892
|
formatOutput,
|
|
814
|
-
formatToon
|
|
1893
|
+
formatToon,
|
|
1894
|
+
resolveChainId
|
|
815
1895
|
};
|
|
816
1896
|
//# sourceMappingURL=index.js.map
|