polkadot-cli 0.1.1 → 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 +45 -1
  2. package/dist/cli.mjs +618 -34
  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
@@ -62,6 +101,10 @@ dot chain add westend --light-client
62
101
  # List configured chains
63
102
  dot chain list
64
103
 
104
+ # Re-fetch metadata after a runtime upgrade
105
+ dot chain update # updates default chain
106
+ dot chain update kusama # updates a specific chain
107
+
65
108
  # Set default chain
66
109
  dot chain default kusama
67
110
 
@@ -86,6 +129,7 @@ Config and metadata caches live in `~/.polkadot/`:
86
129
  ```
87
130
  ~/.polkadot/
88
131
  ├── config.json # chains and default chain
132
+ ├── accounts.json # stored accounts (secrets encrypted at rest — coming soon)
89
133
  └── chains/
90
134
  └── polkadot/
91
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());
@@ -333,6 +388,7 @@ ${BOLD}Usage:${RESET}
333
388
  $ dot chain add <name> --rpc <url> Add a chain via WebSocket RPC
334
389
  $ dot chain add <name> --light-client Add a chain via Smoldot light client
335
390
  $ dot chain remove <name> Remove a chain
391
+ $ dot chain update [name] Re-fetch metadata (default chain if omitted)
336
392
  $ dot chain list List configured chains
337
393
  $ dot chain default <name> Set the default chain
338
394
 
@@ -341,10 +397,12 @@ ${BOLD}Examples:${RESET}
341
397
  $ dot chain add westend --light-client
342
398
  $ dot chain default kusama
343
399
  $ dot chain list
400
+ $ dot chain update
401
+ $ dot chain update kusama
344
402
  $ dot chain remove kusama
345
403
  `.trimStart();
346
404
  function registerChainCommands(cli) {
347
- cli.command("chain [action] [name]", "Manage chains (add, remove, list, default)").action(async (action, name, opts) => {
405
+ cli.command("chain [action] [name]", "Manage chains (add, remove, update, list, default)").action(async (action, name, opts) => {
348
406
  if (!action) {
349
407
  console.log(CHAIN_HELP);
350
408
  return;
@@ -356,6 +414,8 @@ function registerChainCommands(cli) {
356
414
  return chainRemove(name);
357
415
  case "list":
358
416
  return chainList();
417
+ case "update":
418
+ return chainUpdate(name, opts);
359
419
  case "default":
360
420
  return chainDefault(name);
361
421
  default:
@@ -381,17 +441,18 @@ async function chainAdd(name, opts) {
381
441
  console.error(" dot chain add <name> --light-client");
382
442
  process.exit(1);
383
443
  }
384
- const config = await loadConfig();
385
- config.chains[name] = {
444
+ const chainConfig = {
386
445
  rpc: opts.rpc ?? "",
387
446
  ...opts.lightClient ? { lightClient: true } : {}
388
447
  };
389
- await saveConfig(config);
390
448
  console.log(`Connecting to ${name}...`);
391
- const clientHandle = await createChainClient(name, config.chains[name], opts.rpc);
449
+ const clientHandle = await createChainClient(name, chainConfig, opts.rpc);
392
450
  try {
393
451
  console.log("Fetching metadata...");
394
452
  await fetchMetadataFromChain(clientHandle, name);
453
+ const config = await loadConfig();
454
+ config.chains[name] = chainConfig;
455
+ await saveConfig(config);
395
456
  console.log(`Chain "${name}" added successfully.`);
396
457
  } finally {
397
458
  clientHandle.destroy();
@@ -429,6 +490,19 @@ async function chainList() {
429
490
  }
430
491
  console.log();
431
492
  }
493
+ async function chainUpdate(name, opts) {
494
+ const config = await loadConfig();
495
+ const { name: chainName, chain: chainConfig } = resolveChain(config, name);
496
+ console.log(`Connecting to ${chainName}...`);
497
+ const clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
498
+ try {
499
+ console.log("Fetching metadata...");
500
+ await fetchMetadataFromChain(clientHandle, chainName);
501
+ console.log(`Metadata for "${chainName}" updated.`);
502
+ } finally {
503
+ clientHandle.destroy();
504
+ }
505
+ }
432
506
  async function chainDefault(name) {
433
507
  if (!name) {
434
508
  console.error("Usage: dot chain default <name>");
@@ -589,6 +663,26 @@ function registerInspectCommand(cli) {
589
663
  });
590
664
  }
591
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
+
592
686
  // src/commands/query.ts
593
687
  var DEFAULT_LIMIT = 100;
594
688
  function registerQueryCommand(cli) {
@@ -625,7 +719,7 @@ function registerQueryCommand(cli) {
625
719
  }
626
720
  const unsafeApi = clientHandle.client.getUnsafeApi();
627
721
  const storageApi = unsafeApi.query[palletInfo.name][storageItem.name];
628
- const parsedKeys = keys.map(parseKeyArg);
722
+ const parsedKeys = keys.map(parseValue);
629
723
  const format = opts.output ?? "pretty";
630
724
  if (storageItem.type === "map" && parsedKeys.length === 0) {
631
725
  const entries = await storageApi.getEntries();
@@ -649,24 +743,6 @@ ${DIM}Showing ${limit} of ${entries.length} entries. Use --limit 0 for all.${RES
649
743
  }
650
744
  });
651
745
  }
652
- function parseKeyArg(arg) {
653
- if (/^\d+$/.test(arg))
654
- return parseInt(arg, 10);
655
- if (/^\d{16,}$/.test(arg))
656
- return BigInt(arg);
657
- if (/^0x[0-9a-fA-F]+$/.test(arg))
658
- return arg;
659
- if (arg === "true")
660
- return true;
661
- if (arg === "false")
662
- return false;
663
- if (arg.startsWith("{") || arg.startsWith("[")) {
664
- try {
665
- return JSON.parse(arg);
666
- } catch {}
667
- }
668
- return arg;
669
- }
670
746
 
671
747
  // src/commands/const.ts
672
748
  function registerConstCommand(cli) {
@@ -705,6 +781,512 @@ function registerConstCommand(cli) {
705
781
  });
706
782
  }
707
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
+
708
1290
  // src/utils/errors.ts
709
1291
  class CliError2 extends Error {
710
1292
  constructor(message) {
@@ -725,8 +1307,10 @@ registerChainCommands(cli);
725
1307
  registerInspectCommand(cli);
726
1308
  registerQueryCommand(cli);
727
1309
  registerConstCommand(cli);
1310
+ registerAccountCommands(cli);
1311
+ registerTxCommand(cli);
728
1312
  cli.help();
729
- cli.version("0.1.1");
1313
+ cli.version("0.2.0");
730
1314
  function handleError(err) {
731
1315
  if (err instanceof CliError2) {
732
1316
  console.error(`Error: ${err.message}`);
@@ -739,8 +1323,8 @@ function handleError(err) {
739
1323
  }
740
1324
  process.on("unhandledRejection", handleError);
741
1325
  try {
742
- const parsed = cli.parse();
743
- if (parsed.args.length === 0 && !parsed.options.help && !parsed.options.version) {
1326
+ cli.parse();
1327
+ if (!cli.matchedCommandName && !cli.options.help && !cli.options.version) {
744
1328
  cli.outputHelp();
745
1329
  }
746
1330
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "0.1.1",
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",