polkadot-cli 0.10.0 → 0.12.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 +98 -13
  2. package/dist/cli.mjs +403 -130
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,26 @@
4
4
 
5
5
  A command-line tool for interacting with Polkadot-ecosystem chains. Manage chains and accounts, query storage, look up constants, inspect metadata, submit extrinsics, and compute hashes — all from your terminal.
6
6
 
7
- Ships with Polkadot as the default chain. Add any Substrate-based chain by pointing to its RPC endpoint.
7
+ Ships with Polkadot and all system parachains preconfigured with multiple fallback RPC endpoints. Add any Substrate-based chain by pointing to its RPC endpoint(s).
8
+
9
+ ### Preconfigured chains
10
+
11
+ | Network | Chain | Light client |
12
+ |---------|-------|:---:|
13
+ | Polkadot | `polkadot` (relay, default) | yes |
14
+ | | `polkadot-asset-hub` | yes |
15
+ | | `polkadot-bridge-hub` | yes |
16
+ | | `polkadot-collectives` | yes |
17
+ | | `polkadot-coretime` | yes |
18
+ | | `polkadot-people` | yes |
19
+ | Paseo (testnet) | `paseo` (relay) | yes |
20
+ | | `paseo-asset-hub` | yes |
21
+ | | `paseo-bridge-hub` | — |
22
+ | | `paseo-collectives` | — |
23
+ | | `paseo-coretime` | yes |
24
+ | | `paseo-people` | yes |
25
+
26
+ Each chain ships with multiple RPC endpoints from decentralized infrastructure providers (IBP, Dotters, Dwellir, and others). The CLI automatically falls back to the next endpoint if the primary is unreachable.
8
27
 
9
28
  ## Install
10
29
 
@@ -19,13 +38,21 @@ This installs the `dot` command globally.
19
38
  ### Manage chains
20
39
 
21
40
  ```bash
22
- # Add a chain
41
+ # Show chain help
42
+ dot chain # shows available actions
43
+ dot chains # shorthand, same as above
44
+
45
+ # Add a chain (single RPC)
23
46
  dot chain add kusama --rpc wss://kusama-rpc.polkadot.io
47
+
48
+ # Add a chain with fallback RPCs (repeat --rpc for each endpoint)
49
+ dot chain add kusama --rpc wss://kusama-rpc.polkadot.io --rpc wss://kusama-rpc.dwellir.com
50
+
51
+ # Add a chain via light client
24
52
  dot chain add westend --light-client
25
53
 
26
54
  # List configured chains
27
- dot chains # shorthand
28
- dot chain list # equivalent
55
+ dot chain list
29
56
 
30
57
  # Re-fetch metadata after a runtime upgrade
31
58
  dot chain update # updates default chain
@@ -45,24 +72,37 @@ Dev accounts (Alice, Bob, Charlie, Dave, Eve, Ferdie) are always available for t
45
72
  > **Security warning:** Account secrets (mnemonics and seeds) are currently stored **unencrypted** in `~/.polkadot/accounts.json`. Do not use this for high-value accounts on mainnet. Encrypted storage is planned for a future release. Use `--env` to keep secrets off disk entirely.
46
73
 
47
74
  ```bash
75
+ # Show account help
76
+ dot account # shows available actions
77
+ dot accounts # shorthand, same as above
78
+
48
79
  # List all accounts (dev + stored)
49
- dot accounts # shorthand
50
- dot account list # equivalent
80
+ dot account list
51
81
 
52
82
  # Create a new account (generates a mnemonic)
53
83
  dot account create my-validator
54
84
 
85
+ # Create with a derivation path
86
+ dot account create my-staking --path //staking
87
+
55
88
  # Import from a BIP39 mnemonic
56
89
  dot account import treasury --secret "word1 word2 ... word12"
57
90
 
58
- # Add an env-var-backed account (secret stays off disk)
59
- dot account add ci-signer --env MY_SECRET
91
+ # Import with a derivation path
92
+ dot account import hot-wallet --secret "word1 word2 ... word12" --path //hot
93
+
94
+ # Import an env-var-backed account (secret stays off disk)
95
+ dot account import ci-signer --env MY_SECRET
96
+
97
+ # Derive a child account from an existing one
98
+ dot account derive treasury treasury-staking --path //staking
60
99
 
61
100
  # Use it — the env var is read at signing time
62
101
  MY_SECRET="word1 word2 ..." dot tx System.remark 0xdead --from ci-signer
63
102
 
64
- # Remove an account
103
+ # Remove one or more accounts
65
104
  dot account remove my-validator
105
+ dot account delete my-validator stale-key
66
106
  ```
67
107
 
68
108
  #### Env-var-backed accounts
@@ -70,11 +110,40 @@ dot account remove my-validator
70
110
  For CI/CD and security-conscious workflows, store a reference to an environment variable instead of the secret itself:
71
111
 
72
112
  ```bash
73
- dot account add ci-signer --env MY_SECRET
113
+ dot account import ci-signer --env MY_SECRET
74
114
  ```
75
115
 
116
+ `--secret` and `--env` are mutually exclusive. `add` is an alias for `import`.
117
+
76
118
  The secret is never written to disk. At signing time, the CLI reads `$MY_SECRET` and derives the keypair. If the variable is not set, the CLI errors with a clear message. `account list` shows an `(env: MY_SECRET)` badge and resolves the address live when the variable is available.
77
119
 
120
+ #### Derivation paths
121
+
122
+ Use `--path` with `create`, `import`, or the `derive` action to derive child keys from the same secret. Different paths produce different keypairs, enabling key separation (e.g. staking vs. governance) without managing multiple mnemonics.
123
+
124
+ ```bash
125
+ # Create with a derivation path
126
+ dot account create my-staking --path //staking
127
+
128
+ # Multi-segment path (hard + soft junctions)
129
+ dot account create multi --path //polkadot//0/wallet
130
+
131
+ # Import with a path
132
+ dot account import hot --secret "word1 word2 ..." --path //hot
133
+
134
+ # Derive a child from an existing account
135
+ dot account derive treasury treasury-staking --path //staking
136
+ ```
137
+
138
+ `derive` copies the source account's secret and applies the given path. It requires both a source name, a new name, and `--path`. Works with env-backed accounts too — the derived account shares the same env var reference.
139
+
140
+ `account list` shows the derivation path next to the account name:
141
+
142
+ ```
143
+ treasury-staking (//staking) 5FHneW46...
144
+ ci-signer (//ci) (env: MY_SECRET) 5EPCUjPx...
145
+ ```
146
+
78
147
  **Supported secret formats for import:**
79
148
 
80
149
  | Format | Example | Status |
@@ -96,6 +165,8 @@ dot inspect kusama.System
96
165
  dot inspect kusama.System.Account
97
166
  ```
98
167
 
168
+ Chain names are case-insensitive — `Polkadot.System.Account`, `POLKADOT.System.Account`, and `polkadot.System.Account` all resolve the same way. The same applies to `--chain Polkadot` and `dot chain default Polkadot`.
169
+
99
170
  The `--chain` flag and default chain still work as before. If both a chain prefix and `--chain` flag are provided, the CLI errors.
100
171
 
101
172
  ### Query storage
@@ -276,12 +347,24 @@ dot hash blake2b256 0xdeadbeef --output json
276
347
 
277
348
  Run `dot hash` with no arguments to see all available algorithms.
278
349
 
350
+ ### Getting help
351
+
352
+ Every command supports `--help` to show its detailed usage, available actions, and examples:
353
+
354
+ ```bash
355
+ dot --help # global help with all commands
356
+ dot account --help # same as `dot account` — shows account actions
357
+ dot chain --help # same as `dot chain` — shows chain actions
358
+ dot hash --help # same as `dot hash` — shows algorithms and examples
359
+ ```
360
+
279
361
  ### Global options
280
362
 
281
363
  | Flag | Description |
282
364
  |------|-------------|
365
+ | `--help` | Show help (global or command-specific) |
283
366
  | `--chain <name>` | Target chain (default from config) |
284
- | `--rpc <url>` | Override RPC endpoint for this call |
367
+ | `--rpc <url>` | Override RPC endpoint(s) for this call (repeat for fallback) |
285
368
  | `--light-client` | Use Smoldot light client |
286
369
  | `--output json` | Raw JSON output (default: pretty) |
287
370
  | `--limit <n>` | Max entries for map queries (0 = unlimited, default: 100) |
@@ -314,13 +397,15 @@ After each command, the CLI checks whether a newer version is available on npm a
314
397
  ╰───────────────────────────────────────────────╯
315
398
  ```
316
399
 
317
- The version check runs in the background on startup and caches the result for 24 hours in `~/.polkadot/update-check.json`. It never blocks the CLI.
400
+ The version check runs in the background on startup and caches the result for 24 hours in `~/.polkadot/update-check.json`. Before exiting, the CLI waits up to 500ms for the check to finish so the cache file is written — even for fast commands like `--help` and `--version`. Long-running commands (queries, transactions) are unaffected since the check completes well before they finish.
401
+
402
+ If the network is unreachable, the failed check is cached for 1 hour so subsequent runs don't incur the 500ms wait repeatedly.
318
403
 
319
404
  The notification is automatically suppressed when:
320
405
 
321
406
  - `DOT_NO_UPDATE_CHECK=1` is set
322
407
  - `CI` environment variable is set (any value)
323
- - stdout is not a TTY (e.g. piped output)
408
+ - stderr is not a TTY (e.g. piped output)
324
409
 
325
410
  ## Configuration
326
411
 
package/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  // src/cli.ts
6
6
  import cac from "cac";
7
7
  // package.json
8
- var version = "0.10.0";
8
+ var version = "0.12.0";
9
9
 
10
10
  // src/config/accounts-store.ts
11
11
  import { access as access2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
@@ -17,14 +17,120 @@ import { homedir } from "node:os";
17
17
  import { join } from "node:path";
18
18
 
19
19
  // src/config/types.ts
20
+ function primaryRpc(rpc) {
21
+ return Array.isArray(rpc) ? rpc[0] : rpc;
22
+ }
20
23
  var DEFAULT_CONFIG = {
21
24
  defaultChain: "polkadot",
22
25
  chains: {
23
26
  polkadot: {
24
- rpc: "wss://rpc.polkadot.io"
27
+ rpc: [
28
+ "wss://polkadot.ibp.network",
29
+ "wss://polkadot.dotters.network",
30
+ "wss://polkadot-rpc.n.dwellir.com",
31
+ "wss://polkadot-rpc.publicnode.com",
32
+ "wss://rpc-polkadot.luckyfriday.io",
33
+ "wss://polkadot.api.onfinality.io/public-ws",
34
+ "wss://rpc-polkadot.helixstreet.io",
35
+ "wss://polkadot-rpc-tn.dwellir.com",
36
+ "wss://polkadot.public.curie.radiumblock.co/ws",
37
+ "wss://rpc-polkadot.stakeworld.io",
38
+ "wss://polkadot.rpc.subquery.network/public/ws",
39
+ "wss://rpc.polkadot.io"
40
+ ]
41
+ },
42
+ "polkadot-asset-hub": {
43
+ rpc: [
44
+ "wss://polkadot-asset-hub-rpc.polkadot.io",
45
+ "wss://asset-hub-polkadot.ibp.network",
46
+ "wss://asset-hub-polkadot.dotters.network",
47
+ "wss://asset-hub-polkadot-rpc.n.dwellir.com",
48
+ "wss://rpc-asset-hub-polkadot.luckyfriday.io",
49
+ "wss://statemint.api.onfinality.io/public-ws",
50
+ "wss://statemint-rpc-tn.dwellir.com",
51
+ "wss://statemint.public.curie.radiumblock.co/ws",
52
+ "wss://asset-hub-polkadot.rpc.permanence.io"
53
+ ]
54
+ },
55
+ "polkadot-bridge-hub": {
56
+ rpc: [
57
+ "wss://polkadot-bridge-hub-rpc.polkadot.io",
58
+ "wss://bridge-hub-polkadot.ibp.network",
59
+ "wss://bridge-hub-polkadot.dotters.network",
60
+ "wss://bridge-hub-polkadot-rpc.n.dwellir.com",
61
+ "wss://rpc-bridge-hub-polkadot.luckyfriday.io",
62
+ "wss://bridgehub-polkadot.api.onfinality.io/public-ws",
63
+ "wss://polkadot-bridge-hub-rpc-tn.dwellir.com",
64
+ "wss://bridgehub-polkadot.public.curie.radiumblock.co/ws"
65
+ ]
66
+ },
67
+ "polkadot-collectives": {
68
+ rpc: [
69
+ "wss://polkadot-collectives-rpc.polkadot.io",
70
+ "wss://collectives-polkadot.ibp.network",
71
+ "wss://collectives-polkadot.dotters.network",
72
+ "wss://collectives-polkadot-rpc.n.dwellir.com",
73
+ "wss://rpc-collectives-polkadot.luckyfriday.io",
74
+ "wss://collectives.api.onfinality.io/public-ws",
75
+ "wss://polkadot-collectives-rpc-tn.dwellir.com",
76
+ "wss://collectives.public.curie.radiumblock.co/ws"
77
+ ]
78
+ },
79
+ "polkadot-coretime": {
80
+ rpc: [
81
+ "wss://polkadot-coretime-rpc.polkadot.io",
82
+ "wss://coretime-polkadot.ibp.network",
83
+ "wss://coretime-polkadot.dotters.network",
84
+ "wss://coretime-polkadot-rpc.n.dwellir.com",
85
+ "wss://rpc-coretime-polkadot.luckyfriday.io",
86
+ "wss://coretime-polkadot.api.onfinality.io/public-ws"
87
+ ]
88
+ },
89
+ "polkadot-people": {
90
+ rpc: [
91
+ "wss://polkadot-people-rpc.polkadot.io",
92
+ "wss://people-polkadot.ibp.network",
93
+ "wss://people-polkadot.dotters.network",
94
+ "wss://people-polkadot-rpc.n.dwellir.com",
95
+ "wss://rpc-people-polkadot.luckyfriday.io",
96
+ "wss://people-polkadot.api.onfinality.io/public-ws"
97
+ ]
98
+ },
99
+ paseo: {
100
+ rpc: [
101
+ "wss://paseo.ibp.network",
102
+ "wss://paseo.dotters.network",
103
+ "wss://paseo-rpc.n.dwellir.com",
104
+ "wss://paseo.rpc.amforc.com"
105
+ ]
106
+ },
107
+ "paseo-asset-hub": {
108
+ rpc: [
109
+ "wss://asset-hub-paseo.ibp.network",
110
+ "wss://asset-hub-paseo.dotters.network",
111
+ "wss://asset-hub-paseo-rpc.n.dwellir.com",
112
+ "wss://sys.turboflakes.io/asset-hub-paseo"
113
+ ]
114
+ },
115
+ "paseo-bridge-hub": {
116
+ rpc: ["wss://bridge-hub-paseo.ibp.network", "wss://bridge-hub-paseo.dotters.network"]
117
+ },
118
+ "paseo-collectives": {
119
+ rpc: ["wss://collectives-paseo.ibp.network", "wss://collectives-paseo.dotters.network"]
120
+ },
121
+ "paseo-coretime": {
122
+ rpc: ["wss://coretime-paseo.ibp.network", "wss://coretime-paseo.dotters.network"]
123
+ },
124
+ "paseo-people": {
125
+ rpc: [
126
+ "wss://people-paseo.ibp.network",
127
+ "wss://people-paseo.dotters.network",
128
+ "wss://people-paseo.rpc.amforc.com"
129
+ ]
25
130
  }
26
131
  }
27
132
  };
133
+ var BUILTIN_CHAIN_NAMES = new Set(Object.keys(DEFAULT_CONFIG.chains));
28
134
 
29
135
  // src/config/store.ts
30
136
  var DOT_DIR = join(homedir(), ".polkadot");
@@ -53,8 +159,11 @@ async function fileExists(path) {
53
159
  async function loadConfig() {
54
160
  await ensureDir(DOT_DIR);
55
161
  if (await fileExists(CONFIG_PATH)) {
56
- const data = await readFile(CONFIG_PATH, "utf-8");
57
- return JSON.parse(data);
162
+ const saved = JSON.parse(await readFile(CONFIG_PATH, "utf-8"));
163
+ return {
164
+ ...saved,
165
+ chains: { ...DEFAULT_CONFIG.chains, ...saved.chains }
166
+ };
58
167
  }
59
168
  await saveConfig(DEFAULT_CONFIG);
60
169
  return DEFAULT_CONFIG;
@@ -80,14 +189,19 @@ async function removeChainData(chainName) {
80
189
  const dir = getChainDir(chainName);
81
190
  await rm(dir, { recursive: true, force: true });
82
191
  }
192
+ function findChainName(config, input) {
193
+ if (config.chains[input])
194
+ return input;
195
+ return Object.keys(config.chains).find((k) => k.toLowerCase() === input.toLowerCase());
196
+ }
83
197
  function resolveChain(config, chainFlag) {
84
- const name = chainFlag ?? config.defaultChain;
85
- const chain = config.chains[name];
86
- if (!chain) {
198
+ const input = chainFlag ?? config.defaultChain;
199
+ const name = findChainName(config, input);
200
+ if (!name) {
87
201
  const available = Object.keys(config.chains).join(", ");
88
- throw new Error(`Unknown chain "${name}". Available chains: ${available}`);
202
+ throw new Error(`Unknown chain "${input}". Available chains: ${available}`);
89
203
  }
90
- return { name, chain };
204
+ return { name, chain: config.chains[name] };
91
205
  }
92
206
 
93
207
  // src/config/accounts-store.ts
@@ -166,24 +280,24 @@ function getDevAddress(name, prefix = 42) {
166
280
  const keypair = getDevKeypair(name);
167
281
  return ss58Address(keypair.publicKey, prefix);
168
282
  }
169
- function createNewAccount() {
283
+ function createNewAccount(path = "") {
170
284
  const mnemonic = generateMnemonic();
171
285
  const entropy = mnemonicToEntropy(mnemonic);
172
286
  const miniSecret = entropyToMiniSecret(entropy);
173
287
  const derive = sr25519CreateDerive(miniSecret);
174
- const keypair = derive("");
288
+ const keypair = derive(path);
175
289
  return { mnemonic, publicKey: keypair.publicKey };
176
290
  }
177
- function importAccount(secret) {
291
+ function importAccount(secret, path = "") {
178
292
  const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(secret);
179
293
  if (isHexSeed) {
180
- const keypair2 = deriveFromHexSeed(secret, "");
294
+ const keypair2 = deriveFromHexSeed(secret, path);
181
295
  return { publicKey: keypair2.publicKey };
182
296
  }
183
297
  if (!validateMnemonic(secret)) {
184
298
  throw new Error("Invalid secret. Expected a 0x-prefixed 32-byte hex seed or a valid BIP39 mnemonic.");
185
299
  }
186
- const keypair = deriveFromMnemonic(secret, "");
300
+ const keypair = deriveFromMnemonic(secret, path);
187
301
  return { publicKey: keypair.publicKey };
188
302
  }
189
303
  function publicKeyToHex(publicKey) {
@@ -210,12 +324,12 @@ function resolveSecret(secret) {
210
324
  }
211
325
  return secret;
212
326
  }
213
- function tryDerivePublicKey(envVarName) {
327
+ function tryDerivePublicKey(envVarName, path = "") {
214
328
  const value = process.env[envVarName];
215
329
  if (!value)
216
330
  return null;
217
331
  try {
218
- const { publicKey } = importAccount(value);
332
+ const { publicKey } = importAccount(value, path);
219
333
  return publicKeyToHex(publicKey);
220
334
  } catch {
221
335
  return null;
@@ -334,39 +448,49 @@ class Spinner {
334
448
  // src/commands/account.ts
335
449
  var ACCOUNT_HELP = `
336
450
  ${BOLD}Usage:${RESET}
337
- $ dot account create <name> Create a new account
338
- $ dot account import <name> --secret <s> Import from BIP39 mnemonic
339
- $ dot account add <name> --env <VAR> Add account backed by env variable
340
- $ dot account list List all accounts
341
- $ dot account remove <name> Remove a stored account
451
+ $ dot account create|new <name> [--path <derivation>] Create a new account
452
+ $ dot account import|add <name> --secret <s> [--path <derivation>] Import from BIP39 mnemonic
453
+ $ dot account import|add <name> --env <VAR> [--path <derivation>] Import account backed by env variable
454
+ $ dot account derive <source> <new-name> --path <derivation> Derive a child account
455
+ $ dot account list List all accounts
456
+ $ dot account remove|delete <name> [name2] ... Remove stored account(s)
342
457
 
343
458
  ${BOLD}Examples:${RESET}
344
459
  $ dot account create my-validator
460
+ $ dot account create my-staking --path //staking
461
+ $ dot account create multi --path //polkadot//0/wallet
345
462
  $ dot account import treasury --secret "word1 word2 ... word12"
346
- $ dot account add ci-signer --env MY_SECRET
463
+ $ dot account import ci-signer --env MY_SECRET --path //ci
464
+ $ dot account derive treasury treasury-staking --path //staking
347
465
  $ dot account list
348
- $ dot account remove my-validator
466
+ $ dot account remove my-validator stale-key
349
467
 
350
468
  ${YELLOW}Note: Secrets are stored unencrypted in ~/.polkadot/accounts.json.
351
469
  Use --env to keep secrets off disk entirely.
352
470
  Hex seed import (0x...) is not supported via CLI.${RESET}
353
471
  `.trimStart();
354
472
  function registerAccountCommands(cli) {
355
- cli.command("account [action] [name]", "Manage local accounts (create, import, add, list, remove)").alias("accounts").option("--secret <value>", "Secret key (mnemonic or hex seed) for import").option("--env <varName>", "Environment variable name holding the secret").action(async (action, name, opts) => {
473
+ cli.command("account [action] [...names]", "Manage local accounts (create, import, list, remove)").alias("accounts").option("--secret <value>", "Secret key (mnemonic or hex seed) for import").option("--env <varName>", "Environment variable name holding the secret").option("--path <derivation>", "Derivation path (e.g. //staking, //polkadot//0/wallet)").action(async (action, names, opts) => {
356
474
  if (!action) {
357
- return accountList();
475
+ if (process.argv[2] === "accounts")
476
+ return accountList();
477
+ console.log(ACCOUNT_HELP);
478
+ return;
358
479
  }
359
480
  switch (action) {
481
+ case "new":
360
482
  case "create":
361
- return accountCreate(name);
483
+ return accountCreate(names[0], opts);
362
484
  case "import":
363
- return accountImport(name, opts);
364
485
  case "add":
365
- return accountAdd(name, opts);
486
+ return accountImport(names[0], opts);
487
+ case "derive":
488
+ return accountDerive(names[0], names[1], opts);
366
489
  case "list":
367
490
  return accountList();
491
+ case "delete":
368
492
  case "remove":
369
- return accountRemove(name);
493
+ return accountRemove(names);
370
494
  default:
371
495
  console.error(`Unknown action "${action}".
372
496
  `);
@@ -375,7 +499,7 @@ function registerAccountCommands(cli) {
375
499
  }
376
500
  });
377
501
  }
378
- async function accountCreate(name) {
502
+ async function accountCreate(name, opts) {
379
503
  if (!name) {
380
504
  console.error(`Account name is required.
381
505
  `);
@@ -389,18 +513,21 @@ async function accountCreate(name) {
389
513
  if (findAccount(accountsFile, name)) {
390
514
  throw new Error(`Account "${name}" already exists.`);
391
515
  }
392
- const { mnemonic, publicKey } = createNewAccount();
516
+ const path = opts.path ?? "";
517
+ const { mnemonic, publicKey } = createNewAccount(path);
393
518
  const hexPub = publicKeyToHex(publicKey);
394
519
  const address = toSs58(publicKey);
395
520
  accountsFile.accounts.push({
396
521
  name,
397
522
  secret: mnemonic,
398
523
  publicKey: hexPub,
399
- derivationPath: ""
524
+ derivationPath: path
400
525
  });
401
526
  await saveAccounts(accountsFile);
402
527
  printHeading("Account Created");
403
528
  console.log(` ${BOLD}Name:${RESET} ${name}`);
529
+ if (path)
530
+ console.log(` ${BOLD}Path:${RESET} ${path}`);
404
531
  console.log(` ${BOLD}Address:${RESET} ${address}`);
405
532
  console.log(` ${BOLD}Mnemonic:${RESET} ${mnemonic}`);
406
533
  console.log();
@@ -414,10 +541,16 @@ async function accountImport(name, opts) {
414
541
  console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
415
542
  process.exit(1);
416
543
  }
417
- if (!opts.secret) {
418
- console.error(`--secret is required.
544
+ if (opts.secret && opts.env) {
545
+ console.error(`Use --secret or --env, not both.
546
+ `);
547
+ process.exit(1);
548
+ }
549
+ if (!opts.secret && !opts.env) {
550
+ console.error(`--secret or --env is required.
419
551
  `);
420
552
  console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
553
+ console.error(" dot account import <name> --env <VAR>");
421
554
  process.exit(1);
422
555
  }
423
556
  if (isDevAccount(name)) {
@@ -427,58 +560,115 @@ async function accountImport(name, opts) {
427
560
  if (findAccount(accountsFile, name)) {
428
561
  throw new Error(`Account "${name}" already exists.`);
429
562
  }
430
- const { publicKey } = importAccount(opts.secret);
431
- const hexPub = publicKeyToHex(publicKey);
432
- const address = toSs58(publicKey);
433
- accountsFile.accounts.push({
434
- name,
435
- secret: opts.secret,
436
- publicKey: hexPub,
437
- derivationPath: ""
438
- });
439
- await saveAccounts(accountsFile);
440
- printHeading("Account Imported");
441
- console.log(` ${BOLD}Name:${RESET} ${name}`);
442
- console.log(` ${BOLD}Address:${RESET} ${address}`);
443
- console.log();
444
- }
445
- async function accountAdd(name, opts) {
446
- if (!name) {
447
- console.error(`Account name is required.
563
+ const path = opts.path ?? "";
564
+ if (opts.env) {
565
+ const publicKey = tryDerivePublicKey(opts.env, path) ?? "";
566
+ accountsFile.accounts.push({
567
+ name,
568
+ secret: { env: opts.env },
569
+ publicKey,
570
+ derivationPath: path
571
+ });
572
+ await saveAccounts(accountsFile);
573
+ printHeading("Account Imported");
574
+ console.log(` ${BOLD}Name:${RESET} ${name}`);
575
+ if (path)
576
+ console.log(` ${BOLD}Path:${RESET} ${path}`);
577
+ console.log(` ${BOLD}Env:${RESET} ${opts.env}`);
578
+ if (publicKey) {
579
+ console.log(` ${BOLD}Address:${RESET} ${toSs58(publicKey)}`);
580
+ } else {
581
+ console.log(` ${YELLOW}Address will resolve when $${opts.env} is set.${RESET}`);
582
+ }
583
+ console.log();
584
+ } else {
585
+ const { publicKey } = importAccount(opts.secret, path);
586
+ const hexPub = publicKeyToHex(publicKey);
587
+ const address = toSs58(publicKey);
588
+ accountsFile.accounts.push({
589
+ name,
590
+ secret: opts.secret,
591
+ publicKey: hexPub,
592
+ derivationPath: path
593
+ });
594
+ await saveAccounts(accountsFile);
595
+ printHeading("Account Imported");
596
+ console.log(` ${BOLD}Name:${RESET} ${name}`);
597
+ if (path)
598
+ console.log(` ${BOLD}Path:${RESET} ${path}`);
599
+ console.log(` ${BOLD}Address:${RESET} ${address}`);
600
+ console.log();
601
+ }
602
+ }
603
+ async function accountDerive(sourceName, newName, opts) {
604
+ if (!sourceName) {
605
+ console.error(`Source account name is required.
448
606
  `);
449
- console.error("Usage: dot account add <name> --env <VAR>");
607
+ console.error("Usage: dot account derive <source> <new-name> --path <derivation>");
450
608
  process.exit(1);
451
609
  }
452
- if (!opts.env) {
453
- console.error(`--env is required.
610
+ if (!newName) {
611
+ console.error(`New account name is required.
454
612
  `);
455
- console.error("Usage: dot account add <name> --env <VAR>");
613
+ console.error("Usage: dot account derive <source> <new-name> --path <derivation>");
456
614
  process.exit(1);
457
615
  }
458
- if (isDevAccount(name)) {
459
- throw new Error(`"${name}" is a built-in dev account and cannot be used as a custom account name.`);
616
+ if (!opts.path) {
617
+ console.error(`--path is required for derive.
618
+ `);
619
+ console.error("Usage: dot account derive <source> <new-name> --path <derivation>");
620
+ process.exit(1);
460
621
  }
461
- const accountsFile = await loadAccounts();
462
- if (findAccount(accountsFile, name)) {
463
- throw new Error(`Account "${name}" already exists.`);
622
+ if (isDevAccount(newName)) {
623
+ throw new Error(`"${newName}" is a built-in dev account and cannot be used as a custom account name.`);
464
624
  }
465
- const publicKey = tryDerivePublicKey(opts.env) ?? "";
466
- accountsFile.accounts.push({
467
- name,
468
- secret: { env: opts.env },
469
- publicKey,
470
- derivationPath: ""
471
- });
472
- await saveAccounts(accountsFile);
473
- printHeading("Account Added");
474
- console.log(` ${BOLD}Name:${RESET} ${name}`);
475
- console.log(` ${BOLD}Env:${RESET} ${opts.env}`);
476
- if (publicKey) {
477
- console.log(` ${BOLD}Address:${RESET} ${toSs58(publicKey)}`);
625
+ const accountsFile = await loadAccounts();
626
+ const source = findAccount(accountsFile, sourceName);
627
+ if (!source) {
628
+ throw new Error(`Source account "${sourceName}" not found.`);
629
+ }
630
+ if (findAccount(accountsFile, newName)) {
631
+ throw new Error(`Account "${newName}" already exists.`);
632
+ }
633
+ const path = opts.path;
634
+ if (isEnvSecret(source.secret)) {
635
+ const publicKey = tryDerivePublicKey(source.secret.env, path) ?? "";
636
+ accountsFile.accounts.push({
637
+ name: newName,
638
+ secret: source.secret,
639
+ publicKey,
640
+ derivationPath: path
641
+ });
642
+ await saveAccounts(accountsFile);
643
+ printHeading("Account Derived");
644
+ console.log(` ${BOLD}Name:${RESET} ${newName}`);
645
+ console.log(` ${BOLD}Source:${RESET} ${sourceName}`);
646
+ console.log(` ${BOLD}Path:${RESET} ${path}`);
647
+ console.log(` ${BOLD}Env:${RESET} ${source.secret.env}`);
648
+ if (publicKey) {
649
+ console.log(` ${BOLD}Address:${RESET} ${toSs58(publicKey)}`);
650
+ } else {
651
+ console.log(` ${YELLOW}Address will resolve when $${source.secret.env} is set.${RESET}`);
652
+ }
653
+ console.log();
478
654
  } else {
479
- console.log(` ${YELLOW}Address will resolve when $${opts.env} is set.${RESET}`);
655
+ const { publicKey } = importAccount(source.secret, path);
656
+ const hexPub = publicKeyToHex(publicKey);
657
+ const address = toSs58(publicKey);
658
+ accountsFile.accounts.push({
659
+ name: newName,
660
+ secret: source.secret,
661
+ publicKey: hexPub,
662
+ derivationPath: path
663
+ });
664
+ await saveAccounts(accountsFile);
665
+ printHeading("Account Derived");
666
+ console.log(` ${BOLD}Name:${RESET} ${newName}`);
667
+ console.log(` ${BOLD}Source:${RESET} ${sourceName}`);
668
+ console.log(` ${BOLD}Path:${RESET} ${path}`);
669
+ console.log(` ${BOLD}Address:${RESET} ${address}`);
670
+ console.log();
480
671
  }
481
- console.log();
482
672
  }
483
673
  async function accountList() {
484
674
  printHeading("Dev Accounts");
@@ -492,12 +682,15 @@ async function accountList() {
492
682
  printHeading("Stored Accounts");
493
683
  for (const account of accountsFile.accounts) {
494
684
  let displayName = account.name;
685
+ if (account.derivationPath) {
686
+ displayName += ` (${account.derivationPath})`;
687
+ }
495
688
  let address;
496
689
  if (isEnvSecret(account.secret)) {
497
690
  displayName += ` (env: ${account.secret.env})`;
498
691
  let pubKey = account.publicKey;
499
692
  if (!pubKey) {
500
- pubKey = tryDerivePublicKey(account.secret.env) ?? "";
693
+ pubKey = tryDerivePublicKey(account.secret.env, account.derivationPath) ?? "";
501
694
  }
502
695
  address = pubKey ? toSs58(pubKey) : "n/a";
503
696
  } else {
@@ -511,24 +704,44 @@ async function accountList() {
511
704
  }
512
705
  console.log();
513
706
  }
514
- async function accountRemove(name) {
515
- if (!name) {
516
- console.error(`Account name is required.
707
+ async function accountRemove(names) {
708
+ if (names.length === 0) {
709
+ console.error(`At least one account name is required.
517
710
  `);
518
- console.error("Usage: dot account remove <name>");
711
+ console.error("Usage: dot account remove <name> [name2] ...");
519
712
  process.exit(1);
520
713
  }
521
- if (isDevAccount(name)) {
522
- throw new Error("Cannot remove built-in dev accounts.");
714
+ const errors = [];
715
+ for (const name of names) {
716
+ if (isDevAccount(name)) {
717
+ errors.push(`Cannot remove built-in dev account "${name}".`);
718
+ }
719
+ }
720
+ if (errors.length > 0) {
721
+ throw new Error(errors.join(`
722
+ `));
523
723
  }
524
724
  const accountsFile = await loadAccounts();
525
- const idx = accountsFile.accounts.findIndex((a) => a.name.toLowerCase() === name.toLowerCase());
526
- if (idx === -1) {
527
- throw new Error(`Account "${name}" not found.`);
725
+ const indicesToRemove = new Set;
726
+ for (const name of names) {
727
+ const idx = accountsFile.accounts.findIndex((a) => a.name.toLowerCase() === name.toLowerCase());
728
+ if (idx === -1) {
729
+ errors.push(`Account "${name}" not found.`);
730
+ } else {
731
+ indicesToRemove.add(idx);
732
+ }
733
+ }
734
+ if (errors.length > 0) {
735
+ throw new Error(errors.join(`
736
+ `));
737
+ }
738
+ for (const idx of [...indicesToRemove].sort((a, b) => b - a)) {
739
+ accountsFile.accounts.splice(idx, 1);
528
740
  }
529
- accountsFile.accounts.splice(idx, 1);
530
741
  await saveAccounts(accountsFile);
531
- console.log(`Account "${name}" removed.`);
742
+ for (const name of names) {
743
+ console.log(`Account "${name}" removed.`);
744
+ }
532
745
  }
533
746
 
534
747
  // src/core/client.ts
@@ -560,10 +773,18 @@ class MetadataError extends CliError {
560
773
 
561
774
  // src/core/client.ts
562
775
  var KNOWN_CHAIN_SPECS = {
563
- polkadot: "polkadot-api/chains/polkadot",
564
- kusama: "polkadot-api/chains/ksmcc3",
565
- westend: "polkadot-api/chains/westend2",
566
- paseo: "polkadot-api/chains/paseo"
776
+ polkadot: { spec: "polkadot-api/chains/polkadot" },
777
+ kusama: { spec: "polkadot-api/chains/ksmcc3" },
778
+ westend: { spec: "polkadot-api/chains/westend2" },
779
+ paseo: { spec: "polkadot-api/chains/paseo" },
780
+ "polkadot-asset-hub": { spec: "polkadot-api/chains/polkadot_asset_hub", relay: "polkadot" },
781
+ "polkadot-bridge-hub": { spec: "polkadot-api/chains/polkadot_bridge_hub", relay: "polkadot" },
782
+ "polkadot-collectives": { spec: "polkadot-api/chains/polkadot_collectives", relay: "polkadot" },
783
+ "polkadot-coretime": { spec: "polkadot-api/chains/polkadot_coretime", relay: "polkadot" },
784
+ "polkadot-people": { spec: "polkadot-api/chains/polkadot_people", relay: "polkadot" },
785
+ "paseo-asset-hub": { spec: "polkadot-api/chains/paseo_asset_hub", relay: "paseo" },
786
+ "paseo-coretime": { spec: "polkadot-api/chains/paseo_coretime", relay: "paseo" },
787
+ "paseo-people": { spec: "polkadot-api/chains/paseo_people", relay: "paseo" }
567
788
  };
568
789
  function suppressWsNoise() {
569
790
  const orig = console.error;
@@ -607,12 +828,22 @@ async function createChainClient(chainName, chainConfig, rpcOverride) {
607
828
  async function createSmoldotProvider(chainName) {
608
829
  const { start } = await import("polkadot-api/smoldot");
609
830
  const { getSmProvider } = await import("polkadot-api/sm-provider");
610
- const specPath = KNOWN_CHAIN_SPECS[chainName];
611
- if (!specPath) {
831
+ const entry = KNOWN_CHAIN_SPECS[chainName];
832
+ if (!entry) {
612
833
  throw new ConnectionError(`Light client is only supported for known chains: ${Object.keys(KNOWN_CHAIN_SPECS).join(", ")}. Use --rpc to connect to "${chainName}" instead.`);
613
834
  }
614
- const { chainSpec } = await import(specPath);
835
+ const { chainSpec } = await import(entry.spec);
615
836
  const smoldot = start();
837
+ if (entry.relay) {
838
+ const relayEntry = KNOWN_CHAIN_SPECS[entry.relay];
839
+ if (!relayEntry) {
840
+ throw new ConnectionError(`Relay chain "${entry.relay}" not found in known chain specs.`);
841
+ }
842
+ const { chainSpec: relaySpec } = await import(relayEntry.spec);
843
+ const relayChain = await smoldot.addChain({ chainSpec: relaySpec, disableJsonRpc: true });
844
+ const chain2 = await smoldot.addChain({ chainSpec, potentialRelayChains: [relayChain] });
845
+ return getSmProvider(chain2);
846
+ }
616
847
  const chain = await smoldot.addChain({ chainSpec });
617
848
  return getSmProvider(chain);
618
849
  }
@@ -774,6 +1005,7 @@ ${BOLD}Usage:${RESET}
774
1005
 
775
1006
  ${BOLD}Examples:${RESET}
776
1007
  $ dot chain add kusama --rpc wss://kusama-rpc.polkadot.io
1008
+ $ dot chain add kusama --rpc wss://kusama-rpc.polkadot.io --rpc wss://kusama-rpc.dwellir.com
777
1009
  $ dot chain add westend --light-client
778
1010
  $ dot chain default kusama
779
1011
  $ dot chain list
@@ -784,7 +1016,10 @@ ${BOLD}Examples:${RESET}
784
1016
  function registerChainCommands(cli) {
785
1017
  cli.command("chain [action] [name]", "Manage chains (add, remove, update, list, default)").alias("chains").action(async (action, name, opts) => {
786
1018
  if (!action) {
787
- return chainList();
1019
+ if (process.argv[2] === "chains")
1020
+ return chainList();
1021
+ console.log(CHAIN_HELP);
1022
+ return;
788
1023
  }
789
1024
  switch (action) {
790
1025
  case "add":
@@ -843,20 +1078,21 @@ async function chainRemove(name) {
843
1078
  process.exit(1);
844
1079
  }
845
1080
  const config = await loadConfig();
846
- if (!config.chains[name]) {
1081
+ const resolved = findChainName(config, name);
1082
+ if (!resolved) {
847
1083
  throw new Error(`Chain "${name}" not found.`);
848
1084
  }
849
- if (name === "polkadot") {
850
- throw new Error('Cannot remove the built-in "polkadot" chain.');
1085
+ if (BUILTIN_CHAIN_NAMES.has(resolved)) {
1086
+ throw new Error(`Cannot remove the built-in "${resolved}" chain.`);
851
1087
  }
852
- delete config.chains[name];
853
- if (config.defaultChain === name) {
1088
+ delete config.chains[resolved];
1089
+ if (config.defaultChain === resolved) {
854
1090
  config.defaultChain = "polkadot";
855
1091
  console.log(`Default chain reset to "polkadot".`);
856
1092
  }
857
1093
  await saveConfig(config);
858
- await removeChainData(name);
859
- console.log(`Chain "${name}" removed.`);
1094
+ await removeChainData(resolved);
1095
+ console.log(`Chain "${resolved}" removed.`);
860
1096
  }
861
1097
  async function chainList() {
862
1098
  const config = await loadConfig();
@@ -864,8 +1100,15 @@ async function chainList() {
864
1100
  for (const [name, chainConfig] of Object.entries(config.chains)) {
865
1101
  const isDefault = name === config.defaultChain;
866
1102
  const marker = isDefault ? ` ${BOLD}(default)${RESET}` : "";
867
- const provider = chainConfig.lightClient ? `${DIM}light-client${RESET}` : `${DIM}${chainConfig.rpc}${RESET}`;
868
- console.log(` ${CYAN}${name}${RESET}${marker} ${provider}`);
1103
+ if (chainConfig.lightClient) {
1104
+ console.log(` ${CYAN}${name}${RESET}${marker} ${DIM}light-client${RESET}`);
1105
+ } else {
1106
+ const rpcs = Array.isArray(chainConfig.rpc) ? chainConfig.rpc : [chainConfig.rpc];
1107
+ console.log(` ${CYAN}${name}${RESET}${marker} ${DIM}${rpcs[0]}${RESET}`);
1108
+ for (let i = 1;i < rpcs.length; i++) {
1109
+ console.log(` ${DIM}${rpcs[i]}${RESET}`);
1110
+ }
1111
+ }
869
1112
  }
870
1113
  console.log();
871
1114
  }
@@ -888,13 +1131,14 @@ async function chainDefault(name) {
888
1131
  process.exit(1);
889
1132
  }
890
1133
  const config = await loadConfig();
891
- if (!config.chains[name]) {
1134
+ const resolved = findChainName(config, name);
1135
+ if (!resolved) {
892
1136
  const available = Object.keys(config.chains).join(", ");
893
1137
  throw new Error(`Chain "${name}" not found. Available: ${available}`);
894
1138
  }
895
- config.defaultChain = name;
1139
+ config.defaultChain = resolved;
896
1140
  await saveConfig(config);
897
- console.log(`Default chain set to "${name}".`);
1141
+ console.log(`Default chain set to "${resolved}".`);
898
1142
  }
899
1143
 
900
1144
  // src/utils/fuzzy-match.ts
@@ -1799,7 +2043,7 @@ function registerTxCommand(cli) {
1799
2043
  }
1800
2044
  }
1801
2045
  }
1802
- const rpcUrl = opts.rpc ?? chainConfig.rpc;
2046
+ const rpcUrl = primaryRpc(opts.rpc ?? chainConfig.rpc);
1803
2047
  if (rpcUrl) {
1804
2048
  const blockHash = result.block.hash;
1805
2049
  console.log(` ${BOLD}Explorer:${RESET}`);
@@ -2339,7 +2583,10 @@ import { join as join3 } from "node:path";
2339
2583
  var CACHE_FILE = "update-check.json";
2340
2584
  var STALE_MS = 24 * 60 * 60 * 1000;
2341
2585
  var FETCH_TIMEOUT_MS = 5000;
2586
+ var EXIT_WAIT_TIMEOUT_MS = 500;
2587
+ var RETRY_AFTER_FAILURE_MS = 60 * 60 * 1000;
2342
2588
  var REGISTRY_URL = "https://registry.npmjs.org/polkadot-cli/latest";
2589
+ var pendingCheck = null;
2343
2590
  function parseSemver(v) {
2344
2591
  const clean = v.replace(/^v/, "").split("-")[0] ?? v;
2345
2592
  const parts = clean.split(".").map(Number);
@@ -2405,20 +2652,32 @@ async function writeCache(cache) {
2405
2652
  `);
2406
2653
  } catch {}
2407
2654
  }
2408
- function startBackgroundCheck(_currentVersion) {
2655
+ function startBackgroundCheck(currentVersion) {
2409
2656
  const cache = readCache();
2410
2657
  const now = Date.now();
2411
2658
  if (cache && now - cache.lastCheck < STALE_MS) {
2412
2659
  return;
2413
2660
  }
2414
- fetch(REGISTRY_URL, {
2661
+ pendingCheck = fetch(REGISTRY_URL, {
2415
2662
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2416
2663
  }).then((res) => res.json()).then((data) => {
2417
2664
  const latestVersion = data.version;
2418
2665
  if (typeof latestVersion === "string") {
2419
- writeCache({ lastCheck: now, latestVersion });
2666
+ return writeCache({ lastCheck: now, latestVersion });
2420
2667
  }
2421
- }).catch(() => {});
2668
+ }).catch(() => {
2669
+ return writeCache({
2670
+ lastCheck: now - STALE_MS + RETRY_AFTER_FAILURE_MS,
2671
+ latestVersion: currentVersion
2672
+ });
2673
+ });
2674
+ }
2675
+ async function waitForPendingCheck() {
2676
+ if (!pendingCheck)
2677
+ return;
2678
+ const timeout = new Promise((resolve) => setTimeout(resolve, EXIT_WAIT_TIMEOUT_MS));
2679
+ await Promise.race([pendingCheck.catch(() => {}), timeout]);
2680
+ pendingCheck = null;
2422
2681
  }
2423
2682
  function getUpdateNotification(currentVersion) {
2424
2683
  if (process.env.DOT_NO_UPDATE_CHECK === "1")
@@ -2460,16 +2719,17 @@ registerConstCommand(cli);
2460
2719
  registerAccountCommands(cli);
2461
2720
  registerTxCommand(cli);
2462
2721
  registerHashCommand(cli);
2463
- cli.help();
2722
+ cli.option("--help, -h", "Display this message");
2464
2723
  cli.version(version);
2465
- function showUpdateAndExit(code) {
2724
+ async function showUpdateAndExit(code) {
2725
+ await waitForPendingCheck();
2466
2726
  const note = getUpdateNotification(version);
2467
2727
  if (note)
2468
2728
  process.stderr.write(`${note}
2469
2729
  `);
2470
2730
  process.exit(code);
2471
2731
  }
2472
- function handleError(err) {
2732
+ async function handleError(err) {
2473
2733
  if (err instanceof CliError2) {
2474
2734
  console.error(`Error: ${err.message}`);
2475
2735
  } else if (err instanceof Error) {
@@ -2477,21 +2737,34 @@ function handleError(err) {
2477
2737
  } else {
2478
2738
  console.error("An unexpected error occurred:", err);
2479
2739
  }
2480
- showUpdateAndExit(1);
2740
+ return showUpdateAndExit(1);
2481
2741
  }
2482
- try {
2483
- cli.parse(process.argv, { run: false });
2484
- if (cli.options.version || cli.options.help) {
2485
- showUpdateAndExit(0);
2486
- } else if (!cli.matchedCommandName) {
2487
- cli.outputHelp();
2488
- showUpdateAndExit(0);
2489
- } else {
2490
- const result = cli.runMatchedCommand();
2491
- if (result && typeof result.then === "function") {
2492
- result.then(() => showUpdateAndExit(0), handleError);
2742
+ async function main() {
2743
+ try {
2744
+ cli.parse(process.argv, { run: false });
2745
+ if (cli.options.version) {
2746
+ await showUpdateAndExit(0);
2747
+ } else if (cli.options.help) {
2748
+ if (cli.matchedCommandName) {
2749
+ const result = cli.runMatchedCommand();
2750
+ if (result && typeof result.then === "function") {
2751
+ await result.then(() => showUpdateAndExit(0), handleError);
2752
+ }
2753
+ } else {
2754
+ cli.outputHelp();
2755
+ await showUpdateAndExit(0);
2756
+ }
2757
+ } else if (!cli.matchedCommandName) {
2758
+ cli.outputHelp();
2759
+ await showUpdateAndExit(0);
2760
+ } else {
2761
+ const result = cli.runMatchedCommand();
2762
+ if (result && typeof result.then === "function") {
2763
+ await result.then(() => showUpdateAndExit(0), handleError);
2764
+ }
2493
2765
  }
2766
+ } catch (err) {
2767
+ await handleError(err);
2494
2768
  }
2495
- } catch (err) {
2496
- handleError(err);
2497
2769
  }
2770
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI tool for querying Polkadot-ecosystem on-chain state",
5
5
  "type": "module",
6
6
  "bin": {