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