polkadot-cli 0.1.2 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +41 -1
  2. package/dist/cli.mjs +597 -31
  3. package/package.json +6 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # polkadot-cli
2
2
 
3
- A command-line tool for querying Polkadot-ecosystem on-chain state. Query storage, look up constants, and inspect chain metadata — all from your terminal.
3
+ A command-line tool for interacting with Polkadot-ecosystem chains. Query storage, look up constants, inspect metadata, manage accounts, and submit extrinsics — all from your terminal.
4
4
 
5
5
  Ships with Polkadot as the default chain. Add any Substrate-based chain by pointing to its RPC endpoint.
6
6
 
@@ -52,6 +52,45 @@ dot inspect System
52
52
  dot inspect System.Account
53
53
  ```
54
54
 
55
+ ### Manage accounts
56
+
57
+ Dev accounts (Alice, Bob, Charlie, Dave, Eve, Ferdie) are always available for testnets. Create or import your own accounts for any chain.
58
+
59
+ ```bash
60
+ # List all accounts (dev + stored)
61
+ dot account list
62
+
63
+ # Create a new account (generates a mnemonic)
64
+ dot account create my-validator
65
+
66
+ # Import from a BIP39 mnemonic
67
+ dot account import treasury --secret "word1 word2 ... word12"
68
+
69
+ # Import from a hex seed
70
+ dot account import raw-key --secret 0xabcdef...
71
+
72
+ # Remove an account
73
+ dot account remove my-validator
74
+ ```
75
+
76
+ ### Submit extrinsics
77
+
78
+ Build, sign, and submit transactions. Arguments are parsed from metadata — the CLI knows the expected types for each call.
79
+
80
+ ```bash
81
+ # Simple remark
82
+ dot tx System.remark 0xdeadbeef --from alice
83
+
84
+ # Transfer (amount in plancks)
85
+ dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 1000000000000 --from alice
86
+
87
+ # Estimate fees without submitting
88
+ dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 1000000000000 --from alice --dry-run
89
+
90
+ # Batch multiple transfers with Utility.batchAll
91
+ dot tx Utility.batchAll '[{"type":"Balances","value":{"type":"transfer_keep_alive","value":{"dest":"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty","value":1000000000000}}},{"type":"Balances","value":{"type":"transfer_keep_alive","value":{"dest":"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y","value":2000000000000}}}]' --from alice
92
+ ```
93
+
55
94
  ### Manage chains
56
95
 
57
96
  ```bash
@@ -90,6 +129,7 @@ Config and metadata caches live in `~/.polkadot/`:
90
129
  ```
91
130
  ~/.polkadot/
92
131
  ├── config.json # chains and default chain
132
+ ├── accounts.json # stored accounts (secrets encrypted at rest — coming soon)
93
133
  └── chains/
94
134
  └── polkadot/
95
135
  └── metadata.bin # cached SCALE-encoded metadata
package/dist/cli.mjs CHANGED
@@ -40,6 +40,9 @@ var DEFAULT_CONFIG = {
40
40
  var DOT_DIR = join(homedir(), ".polkadot");
41
41
  var CONFIG_PATH = join(DOT_DIR, "config.json");
42
42
  var CHAINS_DIR = join(DOT_DIR, "chains");
43
+ function getConfigDir() {
44
+ return DOT_DIR;
45
+ }
43
46
  function getChainDir(chainName) {
44
47
  return join(CHAINS_DIR, chainName);
45
48
  }
@@ -131,17 +134,30 @@ var KNOWN_CHAIN_SPECS = {
131
134
  westend: "polkadot-api/chains/westend2",
132
135
  paseo: "polkadot-api/chains/paseo"
133
136
  };
137
+ function suppressWsNoise() {
138
+ const orig = console.error;
139
+ console.error = (...args) => {
140
+ if (typeof args[0] === "string" && args[0].includes("Unable to connect"))
141
+ return;
142
+ orig(...args);
143
+ };
144
+ return () => {
145
+ console.error = orig;
146
+ };
147
+ }
134
148
  async function createChainClient(chainName, chainConfig, rpcOverride) {
135
149
  const useLight = !rpcOverride && chainConfig.lightClient;
150
+ const restoreConsole = suppressWsNoise();
136
151
  let provider;
137
152
  if (useLight) {
138
153
  provider = await createSmoldotProvider(chainName);
139
154
  } else {
140
155
  const rpc = rpcOverride ?? chainConfig.rpc;
141
156
  if (!rpc) {
157
+ restoreConsole();
142
158
  throw new ConnectionError(`No RPC endpoint configured for chain "${chainName}". Use --rpc or configure one with: dot chain add ${chainName} --rpc <url>`);
143
159
  }
144
- provider = withPolkadotSdkCompat(getWsProvider(rpc));
160
+ provider = withPolkadotSdkCompat(getWsProvider(rpc, { timeout: 1e4 }));
145
161
  }
146
162
  const client = createClient(provider, {
147
163
  getMetadata: async () => loadMetadata(chainName),
@@ -151,7 +167,10 @@ async function createChainClient(chainName, chainConfig, rpcOverride) {
151
167
  });
152
168
  return {
153
169
  client,
154
- destroy: () => client.destroy()
170
+ destroy: () => {
171
+ client.destroy();
172
+ restoreConsole();
173
+ }
155
174
  };
156
175
  }
157
176
  async function createSmoldotProvider(chainName) {
@@ -173,6 +192,7 @@ import {
173
192
  unifyMetadata
174
193
  } from "@polkadot-api/substrate-bindings";
175
194
  import { getLookupFn, getDynamicBuilder } from "@polkadot-api/metadata-builders";
195
+ var METADATA_TIMEOUT_MS = 15000;
176
196
  function parseMetadata(raw) {
177
197
  const decoded = decAnyMetadata(raw);
178
198
  const unified = unifyMetadata(decoded);
@@ -182,10 +202,19 @@ function parseMetadata(raw) {
182
202
  }
183
203
  async function fetchMetadataFromChain(clientHandle, chainName) {
184
204
  const { client } = clientHandle;
185
- const hex = await client._request("state_getMetadata", []);
186
- const bytes = hexToBytes(hex);
187
- await saveMetadata(chainName, bytes);
188
- return bytes;
205
+ try {
206
+ const hex = await Promise.race([
207
+ client._request("state_getMetadata", []),
208
+ new Promise((_, reject) => setTimeout(() => reject(new ConnectionError(`Timed out fetching metadata for "${chainName}" after ${METADATA_TIMEOUT_MS / 1000}s. ` + "Check that the RPC endpoint is correct and reachable.")), METADATA_TIMEOUT_MS))
209
+ ]);
210
+ const bytes = hexToBytes(hex);
211
+ await saveMetadata(chainName, bytes);
212
+ return bytes;
213
+ } catch (err) {
214
+ if (err instanceof ConnectionError)
215
+ throw err;
216
+ throw new ConnectionError(`Failed to fetch metadata for "${chainName}": ${err instanceof Error ? err.message : err}. ` + "Check that the RPC endpoint is correct and reachable.");
217
+ }
189
218
  }
190
219
  async function getOrFetchMetadata(chainName, clientHandle) {
191
220
  let raw = await loadMetadata(chainName);
@@ -213,9 +242,35 @@ function listPallets(meta) {
213
242
  name: c.name,
214
243
  docs: c.docs ?? [],
215
244
  typeId: c.type
216
- }))
245
+ })),
246
+ calls: extractCalls(meta, p.calls)
217
247
  }));
218
248
  }
249
+ function extractCalls(meta, callsRef) {
250
+ if (!callsRef)
251
+ return [];
252
+ try {
253
+ const entry = meta.lookup(callsRef.type);
254
+ if (entry.type !== "enum")
255
+ return [];
256
+ return Object.entries(entry.value).map(([name, variant]) => ({
257
+ name,
258
+ docs: variant.docs ?? [],
259
+ typeId: resolveCallTypeId(variant)
260
+ }));
261
+ } catch {
262
+ return [];
263
+ }
264
+ }
265
+ function resolveCallTypeId(variant) {
266
+ if (variant.type === "lookupEntry")
267
+ return variant.value?.id ?? null;
268
+ if (variant.type === "struct")
269
+ return null;
270
+ if (variant.type === "void" || variant.type === "empty")
271
+ return null;
272
+ return null;
273
+ }
219
274
  function findPallet(meta, palletName) {
220
275
  const pallets = listPallets(meta);
221
276
  return pallets.find((p) => p.name.toLowerCase() === palletName.toLowerCase());
@@ -386,17 +441,18 @@ async function chainAdd(name, opts) {
386
441
  console.error(" dot chain add <name> --light-client");
387
442
  process.exit(1);
388
443
  }
389
- const config = await loadConfig();
390
- config.chains[name] = {
444
+ const chainConfig = {
391
445
  rpc: opts.rpc ?? "",
392
446
  ...opts.lightClient ? { lightClient: true } : {}
393
447
  };
394
- await saveConfig(config);
395
448
  console.log(`Connecting to ${name}...`);
396
- const clientHandle = await createChainClient(name, config.chains[name], opts.rpc);
449
+ const clientHandle = await createChainClient(name, chainConfig, opts.rpc);
397
450
  try {
398
451
  console.log("Fetching metadata...");
399
452
  await fetchMetadataFromChain(clientHandle, name);
453
+ const config = await loadConfig();
454
+ config.chains[name] = chainConfig;
455
+ await saveConfig(config);
400
456
  console.log(`Chain "${name}" added successfully.`);
401
457
  } finally {
402
458
  clientHandle.destroy();
@@ -607,6 +663,26 @@ function registerInspectCommand(cli) {
607
663
  });
608
664
  }
609
665
 
666
+ // src/utils/parse-value.ts
667
+ function parseValue(arg) {
668
+ if (/^\d+$/.test(arg))
669
+ return parseInt(arg, 10);
670
+ if (/^\d{16,}$/.test(arg))
671
+ return BigInt(arg);
672
+ if (/^0x[0-9a-fA-F]+$/.test(arg))
673
+ return arg;
674
+ if (arg === "true")
675
+ return true;
676
+ if (arg === "false")
677
+ return false;
678
+ if (arg.startsWith("{") || arg.startsWith("[")) {
679
+ try {
680
+ return JSON.parse(arg);
681
+ } catch {}
682
+ }
683
+ return arg;
684
+ }
685
+
610
686
  // src/commands/query.ts
611
687
  var DEFAULT_LIMIT = 100;
612
688
  function registerQueryCommand(cli) {
@@ -643,7 +719,7 @@ function registerQueryCommand(cli) {
643
719
  }
644
720
  const unsafeApi = clientHandle.client.getUnsafeApi();
645
721
  const storageApi = unsafeApi.query[palletInfo.name][storageItem.name];
646
- const parsedKeys = keys.map(parseKeyArg);
722
+ const parsedKeys = keys.map(parseValue);
647
723
  const format = opts.output ?? "pretty";
648
724
  if (storageItem.type === "map" && parsedKeys.length === 0) {
649
725
  const entries = await storageApi.getEntries();
@@ -667,24 +743,6 @@ ${DIM}Showing ${limit} of ${entries.length} entries. Use --limit 0 for all.${RES
667
743
  }
668
744
  });
669
745
  }
670
- function parseKeyArg(arg) {
671
- if (/^\d+$/.test(arg))
672
- return parseInt(arg, 10);
673
- if (/^\d{16,}$/.test(arg))
674
- return BigInt(arg);
675
- if (/^0x[0-9a-fA-F]+$/.test(arg))
676
- return arg;
677
- if (arg === "true")
678
- return true;
679
- if (arg === "false")
680
- return false;
681
- if (arg.startsWith("{") || arg.startsWith("[")) {
682
- try {
683
- return JSON.parse(arg);
684
- } catch {}
685
- }
686
- return arg;
687
- }
688
746
 
689
747
  // src/commands/const.ts
690
748
  function registerConstCommand(cli) {
@@ -723,6 +781,512 @@ function registerConstCommand(cli) {
723
781
  });
724
782
  }
725
783
 
784
+ // src/config/accounts-store.ts
785
+ import { join as join2 } from "node:path";
786
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, access as access2 } from "node:fs/promises";
787
+ var ACCOUNTS_PATH = join2(getConfigDir(), "accounts.json");
788
+ async function ensureDir2(dir) {
789
+ await mkdir2(dir, { recursive: true });
790
+ }
791
+ async function fileExists2(path) {
792
+ try {
793
+ await access2(path);
794
+ return true;
795
+ } catch {
796
+ return false;
797
+ }
798
+ }
799
+ async function loadAccounts() {
800
+ await ensureDir2(getConfigDir());
801
+ if (await fileExists2(ACCOUNTS_PATH)) {
802
+ const data = await readFile2(ACCOUNTS_PATH, "utf-8");
803
+ return JSON.parse(data);
804
+ }
805
+ return { accounts: [] };
806
+ }
807
+ async function saveAccounts(file) {
808
+ await ensureDir2(getConfigDir());
809
+ await writeFile2(ACCOUNTS_PATH, JSON.stringify(file, null, 2) + `
810
+ `);
811
+ }
812
+ function findAccount(file, name) {
813
+ return file.accounts.find((a) => a.name.toLowerCase() === name.toLowerCase());
814
+ }
815
+
816
+ // src/core/accounts.ts
817
+ import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
818
+ import {
819
+ DEV_PHRASE,
820
+ mnemonicToEntropy,
821
+ entropyToMiniSecret,
822
+ generateMnemonic,
823
+ validateMnemonic,
824
+ ss58Address
825
+ } from "@polkadot-labs/hdkd-helpers";
826
+ import { getPolkadotSigner } from "polkadot-api/signer";
827
+ var DEV_NAMES = [
828
+ "alice",
829
+ "bob",
830
+ "charlie",
831
+ "dave",
832
+ "eve",
833
+ "ferdie"
834
+ ];
835
+ function isDevAccount(name) {
836
+ return DEV_NAMES.includes(name.toLowerCase());
837
+ }
838
+ function devDerivationPath(name) {
839
+ return "//" + name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
840
+ }
841
+ function deriveFromMnemonic(mnemonic, path) {
842
+ const entropy = mnemonicToEntropy(mnemonic);
843
+ const miniSecret = entropyToMiniSecret(entropy);
844
+ const derive = sr25519CreateDerive(miniSecret);
845
+ return derive(path);
846
+ }
847
+ function deriveFromHexSeed(hexSeed, path) {
848
+ const clean = hexSeed.startsWith("0x") ? hexSeed.slice(2) : hexSeed;
849
+ const seed = new Uint8Array(clean.length / 2);
850
+ for (let i = 0;i < clean.length; i += 2) {
851
+ seed[i / 2] = parseInt(clean.substring(i, i + 2), 16);
852
+ }
853
+ const derive = sr25519CreateDerive(seed);
854
+ return derive(path);
855
+ }
856
+ function getDevKeypair(name) {
857
+ const path = devDerivationPath(name);
858
+ return deriveFromMnemonic(DEV_PHRASE, path);
859
+ }
860
+ function getDevAddress(name, prefix = 42) {
861
+ const keypair = getDevKeypair(name);
862
+ return ss58Address(keypair.publicKey, prefix);
863
+ }
864
+ function createNewAccount() {
865
+ const mnemonic = generateMnemonic();
866
+ const entropy = mnemonicToEntropy(mnemonic);
867
+ const miniSecret = entropyToMiniSecret(entropy);
868
+ const derive = sr25519CreateDerive(miniSecret);
869
+ const keypair = derive("");
870
+ return { mnemonic, publicKey: keypair.publicKey };
871
+ }
872
+ function importAccount(secret) {
873
+ const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(secret);
874
+ if (isHexSeed) {
875
+ const keypair2 = deriveFromHexSeed(secret, "");
876
+ return { publicKey: keypair2.publicKey };
877
+ }
878
+ if (!validateMnemonic(secret)) {
879
+ throw new Error("Invalid secret. Expected a 0x-prefixed 32-byte hex seed or a valid BIP39 mnemonic.");
880
+ }
881
+ const keypair = deriveFromMnemonic(secret, "");
882
+ return { publicKey: keypair.publicKey };
883
+ }
884
+ function publicKeyToHex(publicKey) {
885
+ return "0x" + Array.from(publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
886
+ }
887
+ function toSs58(publicKey, prefix = 42) {
888
+ if (typeof publicKey === "string") {
889
+ const clean = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
890
+ const bytes = new Uint8Array(clean.length / 2);
891
+ for (let i = 0;i < clean.length; i += 2) {
892
+ bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
893
+ }
894
+ return ss58Address(bytes, prefix);
895
+ }
896
+ return ss58Address(publicKey, prefix);
897
+ }
898
+ async function resolveAccountSigner(name) {
899
+ if (isDevAccount(name)) {
900
+ const keypair2 = getDevKeypair(name);
901
+ return getPolkadotSigner(keypair2.publicKey, "Sr25519", keypair2.sign);
902
+ }
903
+ const accountsFile = await loadAccounts();
904
+ const account = findAccount(accountsFile, name);
905
+ if (!account) {
906
+ const available = [
907
+ ...DEV_NAMES,
908
+ ...accountsFile.accounts.map((a) => a.name)
909
+ ];
910
+ throw new Error(`Unknown account "${name}". Available accounts: ${available.join(", ")}`);
911
+ }
912
+ const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(account.secret);
913
+ const keypair = isHexSeed ? deriveFromHexSeed(account.secret, account.derivationPath) : deriveFromMnemonic(account.secret, account.derivationPath);
914
+ return getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign);
915
+ }
916
+
917
+ // src/commands/account.ts
918
+ var ACCOUNT_HELP = `
919
+ ${BOLD}Usage:${RESET}
920
+ $ dot account create <name> Create a new account
921
+ $ dot account import <name> --secret <s> Import from mnemonic or hex seed
922
+ $ dot account list List all accounts
923
+ $ dot account remove <name> Remove a stored account
924
+
925
+ ${BOLD}Examples:${RESET}
926
+ $ dot account create my-validator
927
+ $ dot account import treasury --secret "word1 word2 ... word12"
928
+ $ dot account import raw-key --secret 0xabcdef...
929
+ $ dot account list
930
+ $ dot account remove my-validator
931
+ `.trimStart();
932
+ function registerAccountCommands(cli) {
933
+ cli.command("account [action] [name]", "Manage local accounts (create, import, list, remove)").option("--secret <value>", "Secret key (mnemonic or hex seed) for import").action(async (action, name, opts) => {
934
+ if (!action) {
935
+ console.log(ACCOUNT_HELP);
936
+ return;
937
+ }
938
+ switch (action) {
939
+ case "create":
940
+ return accountCreate(name);
941
+ case "import":
942
+ return accountImport(name, opts);
943
+ case "list":
944
+ return accountList();
945
+ case "remove":
946
+ return accountRemove(name);
947
+ default:
948
+ console.error(`Unknown action "${action}".
949
+ `);
950
+ console.log(ACCOUNT_HELP);
951
+ process.exit(1);
952
+ }
953
+ });
954
+ }
955
+ async function accountCreate(name) {
956
+ if (!name) {
957
+ console.error(`Account name is required.
958
+ `);
959
+ console.error("Usage: dot account create <name>");
960
+ process.exit(1);
961
+ }
962
+ if (isDevAccount(name)) {
963
+ throw new Error(`"${name}" is a built-in dev account and cannot be used as a custom account name.`);
964
+ }
965
+ const accountsFile = await loadAccounts();
966
+ if (findAccount(accountsFile, name)) {
967
+ throw new Error(`Account "${name}" already exists.`);
968
+ }
969
+ const { mnemonic, publicKey } = createNewAccount();
970
+ const hexPub = publicKeyToHex(publicKey);
971
+ const address = toSs58(publicKey);
972
+ accountsFile.accounts.push({
973
+ name,
974
+ secret: mnemonic,
975
+ publicKey: hexPub,
976
+ derivationPath: ""
977
+ });
978
+ await saveAccounts(accountsFile);
979
+ printHeading("Account Created");
980
+ console.log(` ${BOLD}Name:${RESET} ${name}`);
981
+ console.log(` ${BOLD}Address:${RESET} ${address}`);
982
+ console.log(` ${BOLD}Mnemonic:${RESET} ${mnemonic}`);
983
+ console.log();
984
+ console.log(` ${YELLOW}Save this mnemonic phrase! It is the only way to recover this account.${RESET}`);
985
+ console.log();
986
+ }
987
+ async function accountImport(name, opts) {
988
+ if (!name) {
989
+ console.error(`Account name is required.
990
+ `);
991
+ console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
992
+ process.exit(1);
993
+ }
994
+ if (!opts.secret) {
995
+ console.error(`--secret is required.
996
+ `);
997
+ console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
998
+ process.exit(1);
999
+ }
1000
+ if (isDevAccount(name)) {
1001
+ throw new Error(`"${name}" is a built-in dev account and cannot be used as a custom account name.`);
1002
+ }
1003
+ const accountsFile = await loadAccounts();
1004
+ if (findAccount(accountsFile, name)) {
1005
+ throw new Error(`Account "${name}" already exists.`);
1006
+ }
1007
+ const { publicKey } = importAccount(opts.secret);
1008
+ const hexPub = publicKeyToHex(publicKey);
1009
+ const address = toSs58(publicKey);
1010
+ accountsFile.accounts.push({
1011
+ name,
1012
+ secret: opts.secret,
1013
+ publicKey: hexPub,
1014
+ derivationPath: ""
1015
+ });
1016
+ await saveAccounts(accountsFile);
1017
+ printHeading("Account Imported");
1018
+ console.log(` ${BOLD}Name:${RESET} ${name}`);
1019
+ console.log(` ${BOLD}Address:${RESET} ${address}`);
1020
+ console.log();
1021
+ }
1022
+ async function accountList() {
1023
+ printHeading("Dev Accounts");
1024
+ for (const name of DEV_NAMES) {
1025
+ const display = name.charAt(0).toUpperCase() + name.slice(1);
1026
+ const address = getDevAddress(name);
1027
+ printItem(display, address);
1028
+ }
1029
+ const accountsFile = await loadAccounts();
1030
+ if (accountsFile.accounts.length > 0) {
1031
+ printHeading("Stored Accounts");
1032
+ for (const account of accountsFile.accounts) {
1033
+ const address = toSs58(account.publicKey);
1034
+ printItem(account.name, address);
1035
+ }
1036
+ } else {
1037
+ printHeading("Stored Accounts");
1038
+ console.log(" (none)");
1039
+ }
1040
+ console.log();
1041
+ }
1042
+ async function accountRemove(name) {
1043
+ if (!name) {
1044
+ console.error(`Account name is required.
1045
+ `);
1046
+ console.error("Usage: dot account remove <name>");
1047
+ process.exit(1);
1048
+ }
1049
+ if (isDevAccount(name)) {
1050
+ throw new Error("Cannot remove built-in dev accounts.");
1051
+ }
1052
+ const accountsFile = await loadAccounts();
1053
+ const idx = accountsFile.accounts.findIndex((a) => a.name.toLowerCase() === name.toLowerCase());
1054
+ if (idx === -1) {
1055
+ throw new Error(`Account "${name}" not found.`);
1056
+ }
1057
+ accountsFile.accounts.splice(idx, 1);
1058
+ await saveAccounts(accountsFile);
1059
+ console.log(`Account "${name}" removed.`);
1060
+ }
1061
+
1062
+ // src/commands/tx.ts
1063
+ function registerTxCommand(cli) {
1064
+ cli.command("tx [target] [...args]", "Submit an extrinsic (e.g. Balances.transferKeepAlive <dest> <amount>)").option("--from <name>", "Account to sign with (required)").option("--dry-run", "Estimate fees without submitting").action(async (target, args, opts) => {
1065
+ if (!target || !opts.from) {
1066
+ console.log("Usage: dot tx <Pallet.Call> [...args] --from <account> [--dry-run]");
1067
+ console.log("");
1068
+ console.log("Examples:");
1069
+ console.log(" $ dot tx Balances.transferKeepAlive 5FHn... 1000000000000 --from alice");
1070
+ console.log(" $ dot tx System.remark 0xdeadbeef --from alice --dry-run");
1071
+ return;
1072
+ }
1073
+ const config = await loadConfig();
1074
+ const { name: chainName, chain: chainConfig } = resolveChain(config, opts.chain);
1075
+ const { pallet, item: callName } = parseTarget(target);
1076
+ const signer = await resolveAccountSigner(opts.from);
1077
+ const clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
1078
+ try {
1079
+ const meta = await getOrFetchMetadata(chainName, clientHandle);
1080
+ const palletNames = getPalletNames(meta);
1081
+ const palletInfo = findPallet(meta, pallet);
1082
+ if (!palletInfo) {
1083
+ throw new Error(suggestMessage("pallet", pallet, palletNames));
1084
+ }
1085
+ const callInfo = palletInfo.calls.find((c) => c.name.toLowerCase() === callName.toLowerCase());
1086
+ if (!callInfo) {
1087
+ const callNames = palletInfo.calls.map((c) => c.name);
1088
+ throw new Error(suggestMessage(`call in ${palletInfo.name}`, callName, callNames));
1089
+ }
1090
+ const callData = parseCallArgs(meta, palletInfo.name, callInfo.name, args);
1091
+ const unsafeApi = clientHandle.client.getUnsafeApi();
1092
+ const tx = unsafeApi.tx[palletInfo.name][callInfo.name](callData);
1093
+ if (opts.dryRun) {
1094
+ const signerAddress = toSs58(signer.publicKey);
1095
+ console.log(` ${BOLD}From:${RESET} ${opts.from} (${signerAddress})`);
1096
+ try {
1097
+ const fees = await tx.getEstimatedFees(signer.publicKey);
1098
+ console.log(` ${BOLD}Estimated fees:${RESET} ${fees}`);
1099
+ } catch (err) {
1100
+ console.log(` ${BOLD}Estimated fees:${RESET} ${YELLOW}unable to estimate${RESET}`);
1101
+ console.log(` ${DIM}${err.message ?? err}${RESET}`);
1102
+ }
1103
+ return;
1104
+ }
1105
+ console.log("Signing and submitting...");
1106
+ const result = await tx.signAndSubmit(signer);
1107
+ console.log();
1108
+ console.log(` ${BOLD}Tx:${RESET} ${result.txHash}`);
1109
+ if (result.block) {
1110
+ console.log(` ${BOLD}Block:${RESET} #${result.block.number} (${result.block.hash})`);
1111
+ }
1112
+ if (result.dispatchError) {
1113
+ console.log(` ${BOLD}Status:${RESET} ${YELLOW}dispatch error${RESET}`);
1114
+ console.log(` ${BOLD}Error:${RESET} ${result.dispatchError.type}${result.dispatchError.value ? ": " + JSON.stringify(result.dispatchError.value) : ""}`);
1115
+ } else {
1116
+ console.log(` ${BOLD}Status:${RESET} ${GREEN}ok${RESET}`);
1117
+ }
1118
+ if (result.events && result.events.length > 0) {
1119
+ console.log(` ${BOLD}Events:${RESET}`);
1120
+ for (const event of result.events) {
1121
+ const name = `${CYAN}${event.type}${RESET}.${CYAN}${event.value?.type ?? ""}${RESET}`;
1122
+ const payload = event.value?.value;
1123
+ if (payload && typeof payload === "object") {
1124
+ const fields = Object.entries(payload).map(([k, v]) => `${k}: ${formatEventValue(v)}`).join(", ");
1125
+ console.log(` ${name} { ${fields} }`);
1126
+ } else {
1127
+ console.log(` ${name}`);
1128
+ }
1129
+ }
1130
+ }
1131
+ console.log();
1132
+ } finally {
1133
+ clientHandle.destroy();
1134
+ }
1135
+ });
1136
+ }
1137
+ function formatEventValue(v) {
1138
+ if (typeof v === "bigint")
1139
+ return v.toString();
1140
+ if (typeof v === "string")
1141
+ return v;
1142
+ if (typeof v === "number")
1143
+ return v.toString();
1144
+ if (typeof v === "boolean")
1145
+ return v.toString();
1146
+ if (v === null || v === undefined)
1147
+ return "null";
1148
+ return JSON.stringify(v, (_k, val) => typeof val === "bigint" ? val.toString() : val);
1149
+ }
1150
+ function parseCallArgs(meta, palletName, callName, args) {
1151
+ const palletMeta = meta.unified.pallets.find((p) => p.name === palletName);
1152
+ if (!palletMeta?.calls)
1153
+ return;
1154
+ const callsEntry = meta.lookup(palletMeta.calls.type);
1155
+ if (callsEntry.type !== "enum")
1156
+ return;
1157
+ const variant = callsEntry.value[callName];
1158
+ if (!variant)
1159
+ return;
1160
+ if (variant.type === "void") {
1161
+ if (args.length > 0) {
1162
+ throw new Error(`${palletName}.${callName} takes no arguments, but ${args.length} provided.`);
1163
+ }
1164
+ return;
1165
+ }
1166
+ if (variant.type === "struct") {
1167
+ return parseStructArgs(meta.lookup, variant.value, args, `${palletName}.${callName}`);
1168
+ }
1169
+ if (variant.type === "lookupEntry") {
1170
+ const inner = variant.value;
1171
+ if (inner.type === "struct") {
1172
+ return parseStructArgs(meta.lookup, inner.value, args, `${palletName}.${callName}`);
1173
+ }
1174
+ if (inner.type === "void")
1175
+ return;
1176
+ if (args.length !== 1) {
1177
+ throw new Error(`${palletName}.${callName} takes 1 argument (${describeType(meta.lookup, inner.id)}), but ${args.length} provided.`);
1178
+ }
1179
+ return parseTypedArg(meta.lookup, inner, args[0]);
1180
+ }
1181
+ if (variant.type === "tuple") {
1182
+ const entries = variant.value;
1183
+ if (args.length !== entries.length) {
1184
+ throw new Error(`${palletName}.${callName} takes ${entries.length} arguments, but ${args.length} provided.`);
1185
+ }
1186
+ return entries.map((entry, i) => parseTypedArg(meta.lookup, entry, args[i]));
1187
+ }
1188
+ return args.length === 0 ? undefined : args.map(parseValue);
1189
+ }
1190
+ function parseStructArgs(lookup, fields, args, callLabel) {
1191
+ const fieldNames = Object.keys(fields);
1192
+ if (args.length !== fieldNames.length) {
1193
+ const expected = fieldNames.map((name) => `${name}: ${describeType(lookup, fields[name].id)}`).join(", ");
1194
+ throw new Error(`${callLabel} takes ${fieldNames.length} argument(s): ${expected}
1195
+ ` + ` Got ${args.length} argument(s).`);
1196
+ }
1197
+ const result = {};
1198
+ for (let i = 0;i < fieldNames.length; i++) {
1199
+ const name = fieldNames[i];
1200
+ const entry = fields[name];
1201
+ result[name] = parseTypedArg(lookup, entry, args[i]);
1202
+ }
1203
+ return result;
1204
+ }
1205
+ function parseTypedArg(lookup, entry, arg) {
1206
+ switch (entry.type) {
1207
+ case "primitive":
1208
+ return parsePrimitive(entry.value, arg);
1209
+ case "compact":
1210
+ return entry.isBig ? BigInt(arg) : parseInt(arg, 10);
1211
+ case "AccountId32":
1212
+ case "AccountId20":
1213
+ return arg;
1214
+ case "option": {
1215
+ if (arg === "null" || arg === "undefined" || arg === "none") {
1216
+ return;
1217
+ }
1218
+ return parseTypedArg(lookup, entry.value, arg);
1219
+ }
1220
+ case "enum": {
1221
+ if (arg.startsWith("{")) {
1222
+ try {
1223
+ return JSON.parse(arg);
1224
+ } catch {}
1225
+ }
1226
+ const variants = Object.keys(entry.value);
1227
+ const matched = variants.find((v) => v.toLowerCase() === arg.toLowerCase());
1228
+ if (matched) {
1229
+ const variant = entry.value[matched];
1230
+ if (variant.type === "void") {
1231
+ return { type: matched };
1232
+ }
1233
+ }
1234
+ return parseValue(arg);
1235
+ }
1236
+ case "sequence":
1237
+ case "array":
1238
+ if (arg.startsWith("[")) {
1239
+ try {
1240
+ return JSON.parse(arg);
1241
+ } catch {}
1242
+ }
1243
+ if (/^0x[0-9a-fA-F]*$/.test(arg))
1244
+ return arg;
1245
+ return parseValue(arg);
1246
+ case "struct":
1247
+ if (arg.startsWith("{")) {
1248
+ try {
1249
+ return JSON.parse(arg);
1250
+ } catch {}
1251
+ }
1252
+ return parseValue(arg);
1253
+ case "tuple":
1254
+ if (arg.startsWith("[")) {
1255
+ try {
1256
+ return JSON.parse(arg);
1257
+ } catch {}
1258
+ }
1259
+ return parseValue(arg);
1260
+ default:
1261
+ return parseValue(arg);
1262
+ }
1263
+ }
1264
+ function parsePrimitive(prim, arg) {
1265
+ switch (prim) {
1266
+ case "bool":
1267
+ return arg === "true";
1268
+ case "char":
1269
+ case "str":
1270
+ return arg;
1271
+ case "u8":
1272
+ case "u16":
1273
+ case "u32":
1274
+ case "i8":
1275
+ case "i16":
1276
+ case "i32":
1277
+ return parseInt(arg, 10);
1278
+ case "u64":
1279
+ case "u128":
1280
+ case "u256":
1281
+ case "i64":
1282
+ case "i128":
1283
+ case "i256":
1284
+ return BigInt(arg);
1285
+ default:
1286
+ return parseValue(arg);
1287
+ }
1288
+ }
1289
+
726
1290
  // src/utils/errors.ts
727
1291
  class CliError2 extends Error {
728
1292
  constructor(message) {
@@ -743,8 +1307,10 @@ registerChainCommands(cli);
743
1307
  registerInspectCommand(cli);
744
1308
  registerQueryCommand(cli);
745
1309
  registerConstCommand(cli);
1310
+ registerAccountCommands(cli);
1311
+ registerTxCommand(cli);
746
1312
  cli.help();
747
- cli.version("0.1.2");
1313
+ cli.version("0.2.0");
748
1314
  function handleError(err) {
749
1315
  if (err instanceof CliError2) {
750
1316
  console.error(`Error: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tool for querying Polkadot-ecosystem on-chain state",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,11 +32,13 @@
32
32
  "url": "git+https://github.com/peetzweg/polkadot-cli.git"
33
33
  },
34
34
  "dependencies": {
35
- "cac": "^6.7.14",
36
- "polkadot-api": "^1.23.3",
37
35
  "@polkadot-api/metadata-builders": "^0.13.9",
38
36
  "@polkadot-api/substrate-bindings": "^0.17.0",
39
- "@polkadot-api/view-builder": "^0.4.17"
37
+ "@polkadot-api/view-builder": "^0.4.17",
38
+ "@polkadot-labs/hdkd": "^0.0.26",
39
+ "@polkadot-labs/hdkd-helpers": "^0.0.27",
40
+ "cac": "^6.7.14",
41
+ "polkadot-api": "^1.23.3"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@changesets/cli": "^2.29.4",