polkadot-cli 1.14.2 → 1.15.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 +143 -25
  2. package/dist/cli.mjs +360 -114
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ Ships with Polkadot and all system parachains preconfigured with multiple fallba
14
14
  - ✅ zsh, bash, and fish autocompletion
15
15
  - ✅ Exposes all on-chain metadata documentation
16
16
  - ✅ Encode, dry-run, and submit extrinsics
17
- - ✅ Support for custom signed extensions
17
+ - ✅ Support for custom signed extensions — and a `dot <chain>.extensions` inspector to discover them
18
18
  - ✅ Built with agent use in mind — structured JSON output on every command (`--json`)
19
19
  - ✅ Fuzzy matching with typo suggestions
20
20
  - ✅ Account management — BIP39 mnemonics, derivation paths, env-backed secrets, watch-only, dev accounts
@@ -29,6 +29,7 @@ Ships with Polkadot and all system parachains preconfigured with multiple fallba
29
29
  - ✅ Non-native fee payment — pay tx fees in any asset the chain accepts via `--asset` (asset-hub-style chains)
30
30
  - ✅ Bandersnatch member keys — derive Ring VRF member keys from mnemonics for on-chain member sets
31
31
  - ✅ Export/import — portable chain and account configuration for backup, sharing, and CI bootstrapping
32
+ - ✅ Claude Code skill — `dot-cli` skill installable as a plugin marketplace, teaches agents how to drive the CLI
32
33
 
33
34
  ### Preconfigured chains
34
35
 
@@ -57,6 +58,25 @@ npm install -g polkadot-cli@latest
57
58
 
58
59
  This installs the `dot` command globally.
59
60
 
61
+ ## Claude Code skill
62
+
63
+ This repo ships a [Claude Code](https://claude.com/claude-code) skill that teaches Claude how to drive the `dot` CLI — query patterns, tx encoding, runtime API calls, and bash scripting gotchas.
64
+
65
+ Register the marketplace and install the skill:
66
+
67
+ ```
68
+ /plugin marketplace add peetzweg/polkadot-cli
69
+ /plugin install dot-cli@polkadot-cli
70
+ ```
71
+
72
+ The skill auto-triggers when you ask Claude about `dot`, Substrate storage queries, extrinsic submission, runtime APIs, or XCM. You can also invoke it directly with `/dot-cli`.
73
+
74
+ To pull the latest skill updates:
75
+
76
+ ```
77
+ /plugin marketplace update polkadot-cli
78
+ ```
79
+
60
80
  ## Usage
61
81
 
62
82
  ### Manage chains
@@ -133,7 +153,7 @@ Chain names are case-insensitive (`Polkadot.query.System.Number` works the same)
133
153
 
134
154
  #### Export/import chain configuration
135
155
 
136
- Export and import chain configurations for backup, sharing across machines, or team collaboration. Metadata is not included — re-fetch with `dot chain update --all` after importing.
156
+ Export and import chain configurations for backup, sharing across machines, or team collaboration.
137
157
 
138
158
  ```bash
139
159
  # Export custom chains to stdout (pipe-friendly JSON)
@@ -157,12 +177,36 @@ dot chain import my-chains.json --dry-run
157
177
  # Overwrite existing chains
158
178
  dot chain import my-chains.json --overwrite
159
179
 
180
+ # Skip automatic metadata fetch (faster for offline/CI bootstrap)
181
+ dot chain import my-chains.json --no-metadata
182
+
160
183
  # Pipe between machines
161
184
  ssh remote-dev "dot chain export" | dot chain import -
162
185
  ```
163
186
 
164
187
  By default, `export` only includes user-added chains and built-ins with modified RPCs. Use `--all` to include everything. Import skips existing chains unless `--overwrite` is passed, and validates relay references with warnings for missing relays.
165
188
 
189
+ After a non-dry-run import, metadata is fetched automatically for each newly added or overwritten chain so tab completion and metadata-dependent commands work immediately. Pass `--no-metadata` to skip the fetch — you can always backfill later with `dot chain update --all`.
190
+
191
+ Output shows one line per chain with a status glyph and a terse summary:
192
+
193
+ ```
194
+ ✓ preview
195
+ ✓ preview-people
196
+ ⟳ polkadot (overwritten)
197
+ - paseo (skipped)
198
+
199
+ 2 added, 1 overwritten, 1 skipped
200
+
201
+ Updating metadata for 3 chain(s)...
202
+
203
+ ✓ preview
204
+ ✓ preview-people
205
+ ✓ polkadot
206
+ ```
207
+
208
+ Running `dot chain import` with no file path prints the subcommand help instead of blocking on stdin.
209
+
166
210
  ### Manage accounts
167
211
 
168
212
  Dev accounts (Alice, Bob, Charlie, Dave, Eve, Ferdie) are always available for testnets. Create or import your own accounts for any chain.
@@ -187,14 +231,14 @@ dot account create my-validator
187
231
  # Create with a derivation path
188
232
  dot account create my-staking --path //staking
189
233
 
190
- # Import from a BIP39 mnemonic
191
- dot account import treasury --secret "word1 word2 ... word12"
234
+ # Add a keyed account from a BIP39 mnemonic
235
+ dot account add treasury --secret "word1 word2 ... word12"
192
236
 
193
- # Import with a derivation path
194
- dot account import hot-wallet --secret "word1 word2 ... word12" --path //hot
237
+ # Add with a derivation path
238
+ dot account add hot-wallet --secret "word1 word2 ... word12" --path //hot
195
239
 
196
- # Import an env-var-backed account (secret stays off disk)
197
- dot account import ci-signer --env MY_SECRET
240
+ # Add an env-var-backed account (secret stays off disk)
241
+ dot account add ci-signer --env MY_SECRET
198
242
 
199
243
  # Derive a child account from an existing one
200
244
  dot account derive treasury treasury-staking --path //staking
@@ -212,9 +256,9 @@ dot account export --include-secrets --file backup.json
212
256
  dot account export --watch-only
213
257
 
214
258
  # Batch-import accounts from a file
215
- dot account import --file team-accounts.json
216
- dot account import --file accounts.json --dry-run
217
- dot account import --file accounts.json --overwrite
259
+ dot account import team-accounts.json
260
+ dot account import accounts.json --dry-run
261
+ dot account import accounts.json --overwrite
218
262
 
219
263
  # Inspect an account — show public key and SS58 address
220
264
  dot account inspect alice
@@ -236,7 +280,7 @@ dot account add council 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684
236
280
 
237
281
  Watch-only accounts appear in `dot account list` with a `(watch-only)` badge and can be inspected and removed like any other account. They cannot be used with `--from` (signing) or as a source for `derive`.
238
282
 
239
- The `add` subcommand is context-sensitive: bare `add <name> <address>` creates a watch-only entry, while `add --secret` or `add --env` imports a keyed account (same as `import`).
283
+ The `add` subcommand is context-sensitive: bare `add <name> <address>` creates a watch-only entry, while `add --secret` or `add --env` imports a keyed account. `dot account import` is reserved for file-based batch import.
240
284
 
241
285
  #### Named address resolution
242
286
 
@@ -297,21 +341,32 @@ dot account inspect alice --json
297
341
  # {"publicKey":"0xd435...a27d","ss58":"5Grw...utQY","prefix":42,"name":"Alice"}
298
342
  ```
299
343
 
344
+ #### Reveal the sr25519 private key
345
+
346
+ For provisioning another signer (e.g. a server that expects a raw hex private key in an env var), add `--show-secret` to print the **64-byte sr25519 expanded secret** as `0x`-prefixed hex:
347
+
348
+ ```bash
349
+ dot account inspect dave --show-secret
350
+ # Private Key: 0x<128 hex chars> (sr25519 expanded, 64 bytes — never share)
351
+ ```
352
+
353
+ Works for dev accounts (derived on-the-fly from the standard dev mnemonic) and for stored accounts that have a secret (mnemonic or hex seed). Refuses on watch-only accounts, bare SS58 addresses, or hex public keys. The hex is the final secret after any derivation path is applied, so it can be fed directly to signers that don't accept a mnemonic+path (e.g. `@scure/sr25519`'s `sign`, or services like identity-backend that read a `PROXY_PRIVATE_KEY`). Combine with `--json` to include it under the `privateKey` field.
354
+
300
355
  #### Env-var-backed accounts
301
356
 
302
357
  For CI/CD and security-conscious workflows, store a reference to an environment variable instead of the secret itself:
303
358
 
304
359
  ```bash
305
- dot account import ci-signer --env MY_SECRET
360
+ dot account add ci-signer --env MY_SECRET
306
361
  ```
307
362
 
308
- `--secret` and `--env` are mutually exclusive. `add` is an alias for `import`.
363
+ `--secret` and `--env` are mutually exclusive. Use `dot account add` for single-account imports; `dot account import` is reserved for file-based batch import.
309
364
 
310
365
  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.
311
366
 
312
367
  #### Derivation paths
313
368
 
314
- 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.
369
+ Use `--path` with `create`, `add`, 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.
315
370
 
316
371
  ```bash
317
372
  # Create with a derivation path
@@ -320,8 +375,8 @@ dot account create my-staking --path //staking
320
375
  # Multi-segment path (hard + soft junctions)
321
376
  dot account create multi --path //polkadot//0/wallet
322
377
 
323
- # Import with a path
324
- dot account import hot --secret "word1 word2 ..." --path //hot
378
+ # Add with a path
379
+ dot account add hot --secret "word1 word2 ..." --path //hot
325
380
 
326
381
  # Derive a child from an existing account
327
382
  dot account derive treasury treasury-staking --path //staking
@@ -362,20 +417,24 @@ dot account export --include-secrets --file backup.json
362
417
  # Export only watch-only accounts (always safe)
363
418
  dot account export --watch-only
364
419
 
365
- # Batch-import accounts from a file
366
- dot account import --file team-accounts.json
420
+ # Batch-import accounts from a file (positional path, like `dot chain import`)
421
+ dot account import team-accounts.json
367
422
 
368
423
  # Preview without applying
369
- dot account import --file accounts.json --dry-run
424
+ dot account import accounts.json --dry-run
370
425
 
371
426
  # Overwrite existing accounts
372
- dot account import --file accounts.json --overwrite
427
+ dot account import accounts.json --overwrite
373
428
 
374
429
  # Pipe from another machine
375
- ssh remote-dev "dot account export --watch-only" | dot account import --file /dev/stdin
430
+ ssh remote-dev "dot account export --watch-only" | dot account import -
376
431
  ```
377
432
 
378
- Security: default export replaces mnemonic/seed with `"<redacted>"`. `--include-secrets` is required for actual secrets. Env-backed accounts export the variable *name* (e.g. `{"env": "MY_SECRET"}`), never the value. Redacted accounts import as watch-only (public key preserved, no signing capability). The existing single-account `import` (`--secret`/`--env`) is unchanged batch import uses `--file` to distinguish.
433
+ Output mirrors `dot chain import` one line per account with a status glyph (`✓` added, `⟳` overwritten, `-` skipped) and a terse count summary at the end. Running `dot account import` with no file path prints the subcommand help instead of blocking on stdin.
434
+
435
+ `dot account import` is file-only. For a single-account import from a mnemonic or env variable, use `dot account add <name> --secret "..."` or `dot account add <name> --env VAR`.
436
+
437
+ Security: default export replaces mnemonic/seed with `"<redacted>"`. `--include-secrets` is required for actual secrets. Env-backed accounts export the variable *name* (e.g. `{"env": "MY_SECRET"}`), never the value. Redacted accounts import as watch-only (public key preserved, no signing capability).
379
438
 
380
439
  ### Chain prefix
381
440
 
@@ -409,7 +468,7 @@ dot polkadot.apis.Core
409
468
  dot apis Core --chain polkadot
410
469
  ```
411
470
 
412
- This works for all categories (`query`, `tx`, `const`, `events`, `errors`, `apis`). When passing positional method arguments, keep `Pallet` and `Item` either fully dot-joined (`query.System.Account 5Grw...`) or fully space-separated (`query System Account 5Grw...`) — mixing the two (`query System.Account 5Grw...`) does not work because the second arg gets parsed as a pallet name.
471
+ This works for all categories (`query`, `tx`, `const`, `events`, `errors`, `apis`, `extensions`). When passing positional method arguments, keep `Pallet` and `Item` either fully dot-joined (`query.System.Account 5Grw...`) or fully space-separated (`query System Account 5Grw...`) — mixing the two (`query System.Account 5Grw...`) does not work because the second arg gets parsed as a pallet name.
413
472
 
414
473
  ### Query storage
415
474
 
@@ -637,10 +696,44 @@ dot polkadot.const.Balances.ExistentialDeposit # look up value (connects to cha
637
696
  # Runtime APIs
638
697
  dot polkadot.apis # all runtime APIs
639
698
  dot polkadot.apis.Core # methods in Core
699
+
700
+ # Transaction extensions (flat — no pallet sub-level)
701
+ dot polkadot.extensions # all transaction extensions
702
+ dot polkadot.extensions.CheckMortality # extension detail
640
703
  ```
641
704
 
642
705
  `--chain <name>` works as an alternative to the prefix in every form (e.g. `dot tx.Balances --chain polkadot`). To browse pallets across all categories at once, use `dot inspect` (see [Inspect metadata](#inspect-metadata)).
643
706
 
707
+ ### Transaction extensions
708
+
709
+ List the transaction extensions (also known as signed extensions) a chain declares in its runtime, with types and a marker indicating whether `polkadot-api` handles the extension automatically or whether you need to provide a value via `--ext` when building a transaction (see [Submit extrinsics](#submit-extrinsics)).
710
+
711
+ ```bash
712
+ # List all transaction extensions on a chain
713
+ dot polkadot.extensions
714
+
715
+ # Detail view for a single extension
716
+ dot polkadot.extensions.CheckMortality
717
+
718
+ # --chain flag form is equivalent
719
+ dot extensions.ChargeTransactionPayment --chain polkadot
720
+
721
+ # Space-separated syntax also works
722
+ dot extensions CheckMortality --chain polkadot
723
+
724
+ # Structured output for scripts
725
+ dot polkadot.extensions --json
726
+ ```
727
+
728
+ `extension` and `ext` are aliases for `extensions`. Shell completion suggests identifiers after `dot polkadot.extensions.<Tab>`.
729
+
730
+ The list view tags each entry:
731
+
732
+ - `[builtin]` — `polkadot-api` fills this in for you (e.g. `CheckMortality`, `CheckNonce`, `ChargeTransactionPayment`, `CheckMetadataHash`)
733
+ - `[custom]` — you must provide a value with `--ext` when signing, for example `--ext '{"<Identifier>":{"value":<v>}}'`
734
+
735
+ The detail view shows the extension's value type, its `additionalSigned` type, and a ready-to-adapt `--ext` snippet for custom extensions. Use this to discover what `--ext` payload a chain expects before submitting a `dot tx` command.
736
+
644
737
  ### Submit extrinsics
645
738
 
646
739
  Build, sign, and submit transactions. Pass a `Pallet.Call` with arguments, or a raw SCALE-encoded call hex (e.g. from a multisig proposal or governance). Both forms display a decoded human-readable representation of the call.
@@ -806,6 +899,8 @@ For manual override, use `--ext` with a JSON object:
806
899
  dot tx.System.remark 0xdeadbeef --from alice --chain polkadot --ext '{"MyExtension":{"value":"..."}}'
807
900
  ```
808
901
 
902
+ Not sure which extensions a chain exposes? Run `dot <chain>.extensions` (see [Transaction extensions](#transaction-extensions)) to list them all with value types and a `[builtin]` / `[custom]` marker.
903
+
809
904
  #### Transaction options
810
905
 
811
906
  Override low-level transaction parameters. Useful for rapid-fire submission (custom nonce), priority fees (tip), or controlling transaction lifetime (mortality).
@@ -1346,7 +1441,7 @@ The notification is automatically suppressed when:
1346
1441
 
1347
1442
  ## Configuration
1348
1443
 
1349
- Config and metadata caches live in `~/.polkadot/`:
1444
+ Config and metadata caches live in `~/.polkadot/` by default:
1350
1445
 
1351
1446
  ```
1352
1447
  ~/.polkadot/
@@ -1360,6 +1455,29 @@ Config and metadata caches live in `~/.polkadot/`:
1360
1455
 
1361
1456
  > **Warning:** `accounts.json` stores secrets (mnemonics and seeds) in **plain text**. Encrypted-at-rest storage is planned but not yet implemented. Keep appropriate file permissions (`chmod 600 ~/.polkadot/accounts.json`) and do not use this for high-value mainnet accounts.
1362
1457
 
1458
+ ### `DOT_HOME` — redirect the config directory
1459
+
1460
+ Set the `DOT_HOME` environment variable to point at a different directory. When set, the CLI reads and writes **everything** (config, accounts, metadata, update cache) under that path — no `.polkadot` suffix is appended.
1461
+
1462
+ ```bash
1463
+ # Use a scratch directory for experimentation
1464
+ DOT_HOME=/tmp/dot-scratch dot account create throwaway
1465
+
1466
+ # Isolated per-project state (e.g. in a repo-local shell)
1467
+ export DOT_HOME="$PWD/.dot"
1468
+ dot chain add local --rpc ws://localhost:9944
1469
+
1470
+ # Unset or empty DOT_HOME falls back to $HOME/.polkadot
1471
+ ```
1472
+
1473
+ Typical uses:
1474
+
1475
+ - **Run throwaway commands without touching your real accounts.** Point `DOT_HOME` at a tmpdir so `dot account create`, `dot chain add`, and similar never modify `~/.polkadot/`.
1476
+ - **CI and test harnesses.** Give each job its own `DOT_HOME` so parallel runs don't share state. The project's own test fixture (`runCli`) uses this mechanism.
1477
+ - **Multiple profiles on one machine.** Switch between environments (e.g. a mainnet profile and a local-dev profile) by changing `DOT_HOME`.
1478
+
1479
+ Empty-string `DOT_HOME=""` is treated as unset and falls back to `$HOME/.polkadot` — so a shell-quoting slip can't accidentally send writes to `/`.
1480
+
1363
1481
  ## Environment compatibility
1364
1482
 
1365
1483
  The CLI works in Node.js (v22+), Bun, and sandboxed runtimes (e.g. LLM tool-use / MCP environments). WebSocket connections use the native `WebSocket` implementation provided by the runtime — no external WebSocket package is required.
package/dist/cli.mjs CHANGED
@@ -181,13 +181,20 @@ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
181
181
  import { homedir } from "node:os";
182
182
  import { join } from "node:path";
183
183
  function getConfigDir() {
184
- return DOT_DIR;
184
+ const override = process.env.DOT_HOME;
185
+ return override && override.length > 0 ? override : join(homedir(), ".polkadot");
186
+ }
187
+ function getChainsDir() {
188
+ return join(getConfigDir(), "chains");
185
189
  }
186
190
  function getChainDir(chainName) {
187
- return join(CHAINS_DIR, chainName);
191
+ return join(getChainsDir(), chainName);
188
192
  }
189
193
  function getMetadataPath(chainName) {
190
- return join(CHAINS_DIR, chainName, "metadata.bin");
194
+ return join(getChainDir(chainName), "metadata.bin");
195
+ }
196
+ function getConfigPath() {
197
+ return join(getConfigDir(), "config.json");
191
198
  }
192
199
  async function ensureDir(dir) {
193
200
  await mkdir(dir, { recursive: true });
@@ -201,9 +208,10 @@ async function fileExists(path) {
201
208
  }
202
209
  }
203
210
  async function loadConfig() {
204
- await ensureDir(DOT_DIR);
205
- if (await fileExists(CONFIG_PATH)) {
206
- const saved = JSON.parse(await readFile(CONFIG_PATH, "utf-8"));
211
+ await ensureDir(getConfigDir());
212
+ const configPath = getConfigPath();
213
+ if (await fileExists(configPath)) {
214
+ const saved = JSON.parse(await readFile(configPath, "utf-8"));
207
215
  const chains = {};
208
216
  for (const [name, defaultConfig] of Object.entries(DEFAULT_CONFIG.chains)) {
209
217
  chains[name] = saved.chains[name] ? { ...defaultConfig, ...saved.chains[name] } : defaultConfig;
@@ -219,8 +227,8 @@ async function loadConfig() {
219
227
  return DEFAULT_CONFIG;
220
228
  }
221
229
  async function saveConfig(config) {
222
- await ensureDir(DOT_DIR);
223
- await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
230
+ await ensureDir(getConfigDir());
231
+ await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}
224
232
  `);
225
233
  }
226
234
  async function loadMetadata(chainName) {
@@ -255,18 +263,17 @@ function resolveChain(config, chainFlag) {
255
263
  }
256
264
  return { name, chain: config.chains[name] };
257
265
  }
258
- var DOT_DIR, CONFIG_PATH, CHAINS_DIR;
259
266
  var init_store = __esm(() => {
260
267
  init_errors();
261
268
  init_types();
262
- DOT_DIR = join(homedir(), ".polkadot");
263
- CONFIG_PATH = join(DOT_DIR, "config.json");
264
- CHAINS_DIR = join(DOT_DIR, "chains");
265
269
  });
266
270
 
267
271
  // src/config/accounts-store.ts
268
272
  import { access as access2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
269
273
  import { join as join2 } from "node:path";
274
+ function getAccountsPath() {
275
+ return join2(getConfigDir(), "accounts.json");
276
+ }
270
277
  async function ensureDir2(dir) {
271
278
  await mkdir2(dir, { recursive: true });
272
279
  }
@@ -280,24 +287,23 @@ async function fileExists2(path) {
280
287
  }
281
288
  async function loadAccounts() {
282
289
  await ensureDir2(getConfigDir());
283
- if (await fileExists2(ACCOUNTS_PATH)) {
284
- const data = await readFile2(ACCOUNTS_PATH, "utf-8");
290
+ const path = getAccountsPath();
291
+ if (await fileExists2(path)) {
292
+ const data = await readFile2(path, "utf-8");
285
293
  return JSON.parse(data);
286
294
  }
287
295
  return { accounts: [] };
288
296
  }
289
297
  async function saveAccounts(file) {
290
298
  await ensureDir2(getConfigDir());
291
- await writeFile2(ACCOUNTS_PATH, `${JSON.stringify(file, null, 2)}
299
+ await writeFile2(getAccountsPath(), `${JSON.stringify(file, null, 2)}
292
300
  `);
293
301
  }
294
302
  function findAccount(file, name) {
295
303
  return file.accounts.find((a) => a.name.toLowerCase() === name.toLowerCase());
296
304
  }
297
- var ACCOUNTS_PATH;
298
305
  var init_accounts_store = __esm(() => {
299
306
  init_store();
300
- ACCOUNTS_PATH = join2(getConfigDir(), "accounts.json");
301
307
  });
302
308
 
303
309
  // src/config/accounts-types.ts
@@ -355,6 +361,7 @@ import {
355
361
  ss58Decode,
356
362
  validateMnemonic
357
363
  } from "@polkadot-labs/hdkd-helpers";
364
+ import { HDKD, secretFromSeed } from "@scure/sr25519";
358
365
  import { getPolkadotSigner } from "polkadot-api/signer";
359
366
  function isDevAccount(name) {
360
367
  return DEV_NAMES.includes(name.toLowerCase());
@@ -381,6 +388,50 @@ function getDevKeypair(name) {
381
388
  const path = devDerivationPath(name);
382
389
  return deriveFromMnemonic(DEV_PHRASE, path);
383
390
  }
391
+ function parseDerivations(path) {
392
+ const out = [];
393
+ for (const [, type, code] of path.matchAll(DERIVATION_RE)) {
394
+ out.push([type === "//" ? "hard" : "soft", code]);
395
+ }
396
+ return out;
397
+ }
398
+ function createChainCode(code) {
399
+ const chainCode = new Uint8Array(32);
400
+ const asNumber = +code;
401
+ if (Number.isNaN(asNumber)) {
402
+ const bytes = new TextEncoder().encode(code);
403
+ if (bytes.length >= 32) {
404
+ throw new Error(`Derivation component "${code}" is too long (max 31 bytes)`);
405
+ }
406
+ chainCode[0] = bytes.length << 2;
407
+ chainCode.set(bytes, 1);
408
+ } else {
409
+ const n = asNumber >>> 0;
410
+ chainCode[0] = n & 255;
411
+ chainCode[1] = n >>> 8 & 255;
412
+ chainCode[2] = n >>> 16 & 255;
413
+ chainCode[3] = n >>> 24 & 255;
414
+ }
415
+ return chainCode;
416
+ }
417
+ function deriveExpandedSecret(miniSecret, path) {
418
+ return parseDerivations(path).reduce((sk, [type, code]) => type === "hard" ? HDKD.secretHard(sk, createChainCode(code)) : HDKD.secretSoft(sk, createChainCode(code)), secretFromSeed(miniSecret));
419
+ }
420
+ function miniSecretFromSecret(secret) {
421
+ const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(secret);
422
+ if (isHexSeed) {
423
+ const clean = secret.slice(2);
424
+ const bytes = new Uint8Array(32);
425
+ for (let i = 0;i < clean.length; i += 2) {
426
+ bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
427
+ }
428
+ return bytes;
429
+ }
430
+ if (!validateMnemonic(secret)) {
431
+ throw new Error("Invalid secret. Expected a 0x-prefixed 32-byte hex seed or a valid BIP39 mnemonic.");
432
+ }
433
+ return entropyToMiniSecret(mnemonicToEntropy(secret));
434
+ }
384
435
  function getDevAddress(name, prefix = 42) {
385
436
  const keypair = getDevKeypair(name);
386
437
  return ss58Address(keypair.publicKey, prefix);
@@ -474,10 +525,37 @@ async function resolveAccountSigner(name) {
474
525
  const keypair = await resolveAccountKeypair(name);
475
526
  return getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign);
476
527
  }
477
- var DEV_NAMES;
528
+ async function resolveAccountExpandedSecret(name) {
529
+ if (isDevAccount(name)) {
530
+ const miniSecret2 = entropyToMiniSecret(mnemonicToEntropy(DEV_PHRASE));
531
+ return deriveExpandedSecret(miniSecret2, devDerivationPath(name));
532
+ }
533
+ const accountsFile = await loadAccounts();
534
+ const account = findAccount(accountsFile, name);
535
+ if (!account) {
536
+ const available = [...DEV_NAMES, ...accountsFile.accounts.map((a) => a.name)].sort((a, b) => a.localeCompare(b));
537
+ const suggestions = findClosest(name, available);
538
+ const hint = suggestions.length > 0 ? `
539
+ Did you mean: ${suggestions.join(", ")}?` : "";
540
+ const list = available.map((a) => `
541
+ - ${a}`).join("");
542
+ throw new Error(`Unknown account "${name}".${hint}
543
+ Available accounts:${list}`);
544
+ }
545
+ if (account.secret === undefined) {
546
+ throw new Error(`Account "${name}" is watch-only (no secret). Cannot derive private key. Import with --secret or --env.`);
547
+ }
548
+ const miniSecret = miniSecretFromSecret(resolveSecret(account.secret));
549
+ return deriveExpandedSecret(miniSecret, account.derivationPath);
550
+ }
551
+ function bytesToHex(bytes) {
552
+ return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
553
+ }
554
+ var DEV_NAMES, DERIVATION_RE;
478
555
  var init_accounts = __esm(() => {
479
556
  init_accounts_store();
480
557
  DEV_NAMES = ["alice", "bob", "charlie", "dave", "eve", "ferdie"];
558
+ DERIVATION_RE = /(\/{1,2})([^/]+)/g;
481
559
  });
482
560
 
483
561
  // src/utils/binary-display.ts
@@ -556,6 +634,33 @@ function printDocs(docs) {
556
634
  console.log(` ${DIM}${text}${RESET}`);
557
635
  }
558
636
  }
637
+ function printImportResults(params) {
638
+ const { added, overwritten, skipped, dryRun, noun } = params;
639
+ for (const name of added) {
640
+ console.log(` ${GREEN}${CHECK_MARK}${RESET} ${name}`);
641
+ }
642
+ for (const name of overwritten) {
643
+ console.log(` ${YELLOW}⟳${RESET} ${name}${DIM} (overwritten)${RESET}`);
644
+ }
645
+ for (const name of skipped) {
646
+ console.log(` ${DIM}- ${name} (skipped)${RESET}`);
647
+ }
648
+ if (added.length === 0 && overwritten.length === 0 && skipped.length === 0) {
649
+ const prefix = dryRun ? "(dry run) " : "";
650
+ console.log(`${prefix}No ${noun}s imported.`);
651
+ return;
652
+ }
653
+ const parts = [];
654
+ if (added.length > 0)
655
+ parts.push(`${added.length} added`);
656
+ if (overwritten.length > 0)
657
+ parts.push(`${overwritten.length} overwritten`);
658
+ if (skipped.length > 0)
659
+ parts.push(`${skipped.length} skipped`);
660
+ const suffix = dryRun ? " (dry run)" : "";
661
+ console.log();
662
+ console.log(`${parts.join(", ")}${suffix}`);
663
+ }
559
664
 
560
665
  class Spinner {
561
666
  timer = null;
@@ -802,6 +907,22 @@ function getSignedExtensions(meta) {
802
907
  return [];
803
908
  return byVersion[Number(versionKeys[0])] ?? [];
804
909
  }
910
+ function getSignedExtensionNames(meta) {
911
+ return getSignedExtensions(meta).map((e) => e.identifier).sort((a, b) => a.localeCompare(b));
912
+ }
913
+ function findSignedExtension(meta, identifier) {
914
+ return getSignedExtensions(meta).find((e) => e.identifier.toLowerCase() === identifier.toLowerCase());
915
+ }
916
+ function describeSignedExtension(meta, info) {
917
+ return {
918
+ identifier: info.identifier,
919
+ valueType: describeType(meta.lookup, info.type),
920
+ additionalSignedType: describeType(meta.lookup, info.additionalSigned),
921
+ valueTypeId: info.type,
922
+ additionalSignedTypeId: info.additionalSigned,
923
+ isBuiltin: PAPI_BUILTIN_EXTENSIONS.has(info.identifier)
924
+ };
925
+ }
805
926
  function getPalletNames(meta) {
806
927
  return meta.unified.pallets.map((p) => p.name).sort((a, b) => a.localeCompare(b));
807
928
  }
@@ -950,12 +1071,26 @@ function hexToBytes(hex) {
950
1071
  }
951
1072
  return bytes;
952
1073
  }
953
- var METADATA_TIMEOUT_MS = 15000, optionalOpaqueBytes, v15Arg;
1074
+ var METADATA_TIMEOUT_MS = 15000, optionalOpaqueBytes, v15Arg, PAPI_BUILTIN_EXTENSIONS;
954
1075
  var init_metadata = __esm(() => {
955
1076
  init_store();
956
1077
  init_errors();
957
1078
  optionalOpaqueBytes = Option(Bytes());
958
1079
  v15Arg = toHex(u32.enc(15));
1080
+ PAPI_BUILTIN_EXTENSIONS = new Set([
1081
+ "CheckNonZeroSender",
1082
+ "CheckSpecVersion",
1083
+ "CheckTxVersion",
1084
+ "CheckGenesis",
1085
+ "CheckMortality",
1086
+ "CheckNonce",
1087
+ "CheckWeight",
1088
+ "ChargeTransactionPayment",
1089
+ "ChargeAssetTxPayment",
1090
+ "CheckMetadataHash",
1091
+ "StorageWeightReclaim",
1092
+ "PrevalidateAttests"
1093
+ ]);
959
1094
  });
960
1095
 
961
1096
  // src/core/explorers.ts
@@ -1325,7 +1460,7 @@ function parsePrimitive(prim, arg) {
1325
1460
  return parseValue(arg);
1326
1461
  }
1327
1462
  }
1328
- var PAPI_BUILTIN_EXTENSIONS, NO_DEFAULT;
1463
+ var NO_DEFAULT;
1329
1464
  var init_tx = __esm(() => {
1330
1465
  init_store();
1331
1466
  init_types();
@@ -1337,20 +1472,6 @@ var init_tx = __esm(() => {
1337
1472
  init_binary_display();
1338
1473
  init_errors();
1339
1474
  init_focused_inspect();
1340
- PAPI_BUILTIN_EXTENSIONS = new Set([
1341
- "CheckNonZeroSender",
1342
- "CheckSpecVersion",
1343
- "CheckTxVersion",
1344
- "CheckGenesis",
1345
- "CheckMortality",
1346
- "CheckNonce",
1347
- "CheckWeight",
1348
- "ChargeTransactionPayment",
1349
- "ChargeAssetTxPayment",
1350
- "CheckMetadataHash",
1351
- "StorageWeightReclaim",
1352
- "PrevalidateAttests"
1353
- ]);
1354
1475
  NO_DEFAULT = Symbol("no-default");
1355
1476
  });
1356
1477
 
@@ -2046,7 +2167,7 @@ var init_focused_inspect = __esm(() => {
2046
2167
  import { blake2b as blake2b2 } from "@noble/hashes/blake2.js";
2047
2168
  import { sha256 } from "@noble/hashes/sha2.js";
2048
2169
  import { keccak_256 } from "@noble/hashes/sha3.js";
2049
- import { bytesToHex, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
2170
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
2050
2171
  function computeHash(algorithm, data) {
2051
2172
  const algo = ALGORITHMS[algorithm];
2052
2173
  if (!algo) {
@@ -2065,7 +2186,7 @@ function parseInputData(input) {
2065
2186
  return new TextEncoder().encode(input);
2066
2187
  }
2067
2188
  function toHex2(bytes) {
2068
- return `0x${bytesToHex(bytes)}`;
2189
+ return `0x${bytesToHex2(bytes)}`;
2069
2190
  }
2070
2191
  function isValidAlgorithm(name) {
2071
2192
  return name in ALGORITHMS;
@@ -2124,6 +2245,12 @@ async function loadRuntimeApiNames(_config, chainName) {
2124
2245
  methodNames: a.methods.map((m) => m.name)
2125
2246
  }));
2126
2247
  }
2248
+ async function loadExtensionIdentifiers(_config, chainName) {
2249
+ const raw = await loadMetadata(chainName);
2250
+ if (!raw)
2251
+ return null;
2252
+ return getSignedExtensionNames(parseMetadata(raw));
2253
+ }
2127
2254
  function filterPallets(pallets, category) {
2128
2255
  switch (category) {
2129
2256
  case "query":
@@ -2263,6 +2390,9 @@ async function completeDotpath(currentWord, config, knownChains, precedingWords)
2263
2390
  if (category === "apis") {
2264
2391
  return completeApisCategory(first, numComplete, endsWithDot, completeSegments, currentWord, config, chainFromFlag);
2265
2392
  }
2393
+ if (category === "extensions") {
2394
+ return completeExtensionsCategory(first, numComplete, endsWithDot, currentWord, config, chainFromFlag);
2395
+ }
2266
2396
  if (numComplete === 1 && endsWithDot) {
2267
2397
  const chainName = chainFromFlag;
2268
2398
  if (!chainName)
@@ -2319,6 +2449,9 @@ async function completeDotpath(currentWord, config, knownChains, precedingWords)
2319
2449
  if (category === "apis") {
2320
2450
  return completeApisCategory(`${first}.${completeSegments[1]}`, numComplete - 1, endsWithDot, completeSegments.slice(1), currentWord, config, chainName);
2321
2451
  }
2452
+ if (category === "extensions") {
2453
+ return completeExtensionsCategory(`${first}.${completeSegments[1]}`, numComplete - 1, endsWithDot, currentWord, config, chainName);
2454
+ }
2322
2455
  const pallets = await loadPallets(config, chainName);
2323
2456
  if (!pallets)
2324
2457
  return [];
@@ -2375,6 +2508,23 @@ async function completeApisCategory(prefix, numComplete, endsWithDot, segments,
2375
2508
  }
2376
2509
  return [];
2377
2510
  }
2511
+ async function completeExtensionsCategory(prefix, numComplete, endsWithDot, currentWord, config, chainNameOverride) {
2512
+ const chainName = chainNameOverride;
2513
+ if (!chainName)
2514
+ return [];
2515
+ const names = await loadExtensionIdentifiers(config, chainName);
2516
+ if (!names)
2517
+ return [];
2518
+ if (numComplete === 1 && endsWithDot) {
2519
+ const candidates = names.map((n) => `${prefix}.${n}`);
2520
+ return filterPrefix(candidates, currentWord.slice(0, -1));
2521
+ }
2522
+ if (numComplete === 1 && !endsWithDot) {
2523
+ const candidates = names.map((n) => `${prefix}.${n}`);
2524
+ return filterPrefix(candidates, currentWord);
2525
+ }
2526
+ return [];
2527
+ }
2378
2528
  var CATEGORIES2, CATEGORY_ALIASES2, NAMED_COMMANDS, CHAIN_SUBCOMMANDS, ACCOUNT_SUBCOMMANDS, GLOBAL_OPTIONS, TX_OPTIONS, QUERY_OPTIONS;
2379
2529
  var init_complete = __esm(() => {
2380
2530
  init_accounts_store();
@@ -2382,7 +2532,7 @@ var init_complete = __esm(() => {
2382
2532
  init_accounts();
2383
2533
  init_hash();
2384
2534
  init_metadata();
2385
- CATEGORIES2 = ["query", "tx", "const", "events", "errors", "apis"];
2535
+ CATEGORIES2 = ["query", "tx", "const", "events", "errors", "apis", "extensions"];
2386
2536
  CATEGORY_ALIASES2 = {
2387
2537
  query: "query",
2388
2538
  tx: "tx",
@@ -2394,7 +2544,10 @@ var init_complete = __esm(() => {
2394
2544
  errors: "errors",
2395
2545
  error: "errors",
2396
2546
  apis: "apis",
2397
- api: "apis"
2547
+ api: "apis",
2548
+ extensions: "extensions",
2549
+ extension: "extensions",
2550
+ ext: "extensions"
2398
2551
  };
2399
2552
  NAMED_COMMANDS = ["chain", "account", "inspect", "hash", "sign", "parachain", "completions"];
2400
2553
  CHAIN_SUBCOMMANDS = ["add", "remove", "update", "list"];
@@ -2428,7 +2581,7 @@ var init_complete = __esm(() => {
2428
2581
  // src/cli.ts
2429
2582
  import cac from "cac";
2430
2583
  // package.json
2431
- var version = "1.14.2";
2584
+ var version = "1.15.0";
2432
2585
 
2433
2586
  // src/commands/account.ts
2434
2587
  init_accounts_store();
@@ -2441,31 +2594,30 @@ ${BOLD}Usage:${RESET}
2441
2594
  $ dot account add <name> --secret <s> [--path <derivation>] Import from BIP39 mnemonic
2442
2595
  $ dot account add <name> --env <VAR> [--path <derivation>] Import account backed by env variable
2443
2596
  $ dot account create|new <name> [--path <derivation>] Create a new account
2444
- $ dot account import <name> --secret <s> [--path <derivation>] Import from BIP39 mnemonic
2445
- $ dot account import <name> --env <VAR> [--path <derivation>] Import account backed by env variable
2446
- $ dot account import --file <path> Batch-import accounts from a file
2597
+ $ dot account import <file> Batch-import accounts from a file
2447
2598
  $ dot account export [names...] Export accounts to stdout
2448
2599
  $ dot account derive <source> <new-name> --path <derivation> Derive a child account
2449
- $ dot account inspect <input> [--prefix <N>] Inspect an account/address/key
2600
+ $ dot account inspect <input> [--prefix <N>] [--show-secret] Inspect an account/address/key
2450
2601
  $ dot account list List all accounts
2451
2602
  $ dot account remove|delete <name> [name2] ... Remove stored account(s)
2452
2603
 
2453
2604
  ${BOLD}Examples:${RESET}
2454
2605
  $ dot account add treasury 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
2606
+ $ dot account add treasury --secret "word1 word2 ... word12"
2607
+ $ dot account add ci-signer --env MY_SECRET --path //ci
2455
2608
  $ dot account create my-validator
2456
2609
  $ dot account create my-staking --path //staking
2457
2610
  $ dot account create multi --path //polkadot//0/wallet
2458
- $ dot account import treasury --secret "word1 word2 ... word12"
2459
- $ dot account import ci-signer --env MY_SECRET --path //ci
2460
- $ dot account import --file team-accounts.json
2461
- $ dot account import --file accounts.json --dry-run
2462
- $ dot account import --file accounts.json --overwrite
2611
+ $ dot account import team-accounts.json
2612
+ $ dot account import accounts.json --dry-run
2613
+ $ dot account import accounts.json --overwrite
2463
2614
  $ dot account export
2464
2615
  $ dot account export treasury my-validator
2465
2616
  $ dot account export --include-secrets --file backup.json
2466
2617
  $ dot account export --watch-only
2467
2618
  $ dot account derive treasury treasury-staking --path //staking
2468
2619
  $ dot account inspect alice
2620
+ $ dot account inspect dave --show-secret
2469
2621
  $ dot account inspect 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
2470
2622
  $ dot account inspect 0xd435...a27d --prefix 0
2471
2623
  $ dot account list
@@ -2476,7 +2628,7 @@ ${YELLOW}Note: Secrets are stored unencrypted in ~/.polkadot/accounts.json.
2476
2628
  Hex seed import (0x...) is not supported via CLI.${RESET}
2477
2629
  `.trimStart();
2478
2630
  function registerAccountCommands(cli) {
2479
- cli.command("account [action] [...names]", "Manage local accounts (create, import, list, remove, export)").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)").option("--prefix <number>", "SS58 prefix for address encoding (default: 42)").option("--file <path>", "Input/output file for batch import/export").option("--overwrite", "Overwrite existing accounts on batch import").option("--dry-run", "Preview batch import without applying changes").option("--include-secrets", "Include secrets in export (redacted by default)").option("--watch-only", "Export only watch-only accounts").action(async (action, names, opts) => {
2631
+ cli.command("account [action] [...names]", "Manage local accounts (create, import, list, remove, export)").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)").option("--prefix <number>", "SS58 prefix for address encoding (default: 42)").option("--file <path>", "Input/output file for batch import/export").option("--overwrite", "Overwrite existing accounts on batch import").option("--dry-run", "Preview batch import without applying changes").option("--include-secrets", "Include secrets in export (redacted by default)").option("--watch-only", "Export only watch-only accounts").option("--show-secret", "Reveal the 64-byte sr25519 expanded private key (inspect only)").action(async (action, names, opts) => {
2480
2632
  if (!action) {
2481
2633
  if (process.argv[2] === "accounts")
2482
2634
  return accountList(opts);
@@ -2492,9 +2644,7 @@ function registerAccountCommands(cli) {
2492
2644
  return accountImport(names[0], opts);
2493
2645
  return accountAddWatchOnly(names[0], names[1], opts);
2494
2646
  case "import":
2495
- if (opts.file)
2496
- return accountBatchImport(opts);
2497
- return accountImport(names[0], opts);
2647
+ return accountBatchImport(names[0], opts);
2498
2648
  case "export":
2499
2649
  return accountExport(names, opts);
2500
2650
  case "derive":
@@ -2893,16 +3043,19 @@ async function accountInspect(input, opts) {
2893
3043
  let name;
2894
3044
  let publicKeyHex;
2895
3045
  let bandersnatch;
3046
+ let hasSecret = false;
2896
3047
  if (isDevAccount(input)) {
2897
3048
  name = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
2898
3049
  const devAddr = getDevAddress(input);
2899
3050
  publicKeyHex = publicKeyToHex(fromSs58(devAddr));
3051
+ hasSecret = true;
2900
3052
  } else {
2901
3053
  const accountsFile = await loadAccounts();
2902
3054
  const account = findAccount(accountsFile, input);
2903
3055
  if (account) {
2904
3056
  name = account.name;
2905
3057
  bandersnatch = account.bandersnatch;
3058
+ hasSecret = account.secret !== undefined;
2906
3059
  if (account.publicKey) {
2907
3060
  publicKeyHex = account.publicKey;
2908
3061
  } else if (account.secret !== undefined && isEnvSecret(account.secret)) {
@@ -2929,12 +3082,31 @@ async function accountInspect(input, opts) {
2929
3082
  }
2930
3083
  }
2931
3084
  const ss58 = toSs58(publicKeyHex, prefix);
3085
+ let privateKeyHex;
3086
+ if (opts.showSecret) {
3087
+ if (!name) {
3088
+ console.error("--show-secret requires an account name; raw addresses and hex keys have no secret to reveal.");
3089
+ process.exit(1);
3090
+ }
3091
+ if (!hasSecret) {
3092
+ console.error(`Account "${name}" is watch-only (no secret). Cannot reveal private key.`);
3093
+ process.exit(1);
3094
+ }
3095
+ try {
3096
+ privateKeyHex = bytesToHex(await resolveAccountExpandedSecret(input));
3097
+ } catch (err) {
3098
+ console.error(err.message);
3099
+ process.exit(1);
3100
+ }
3101
+ }
2932
3102
  if (isJsonOutput(opts)) {
2933
3103
  const result = { publicKey: publicKeyHex, ss58, prefix };
2934
3104
  if (name)
2935
3105
  result.name = name;
2936
3106
  if (bandersnatch && Object.keys(bandersnatch).length > 0)
2937
3107
  result.bandersnatch = bandersnatch;
3108
+ if (privateKeyHex)
3109
+ result.privateKey = privateKeyHex;
2938
3110
  console.log(formatJson(result));
2939
3111
  } else {
2940
3112
  printHeading("Account Info");
@@ -2955,6 +3127,10 @@ async function accountInspect(input, opts) {
2955
3127
  }
2956
3128
  }
2957
3129
  console.log(` ${BOLD}Prefix:${RESET} ${prefix}`);
3130
+ if (privateKeyHex) {
3131
+ console.log(` ${BOLD}Private Key:${RESET} ${privateKeyHex}`);
3132
+ console.log(` ${YELLOW}(sr25519 expanded, 64 bytes — never share)${RESET}`);
3133
+ }
2958
3134
  console.log();
2959
3135
  }
2960
3136
  }
@@ -3019,12 +3195,17 @@ async function accountExport(names, opts) {
3019
3195
  process.stdout.write(json);
3020
3196
  }
3021
3197
  }
3022
- async function accountBatchImport(opts) {
3198
+ async function accountBatchImport(filePath, opts) {
3199
+ const inputPath = filePath ?? opts.file;
3023
3200
  let raw;
3024
- if (!opts.file || opts.file === "-") {
3201
+ if (!inputPath || inputPath === "-") {
3202
+ if (process.stdin.isTTY) {
3203
+ console.log(ACCOUNT_HELP);
3204
+ return;
3205
+ }
3025
3206
  raw = await readStdin();
3026
3207
  } else {
3027
- raw = await readFile3(opts.file, "utf-8");
3208
+ raw = await readFile3(inputPath, "utf-8");
3028
3209
  }
3029
3210
  let importData;
3030
3211
  try {
@@ -3103,16 +3284,13 @@ async function accountBatchImport(opts) {
3103
3284
  }));
3104
3285
  return;
3105
3286
  }
3106
- const prefix = opts.dryRun ? "(dry run) " : "";
3107
- if (added.length > 0)
3108
- console.log(`${prefix}Added: ${added.join(", ")}`);
3109
- if (overwritten.length > 0)
3110
- console.log(`${prefix}Overwritten: ${overwritten.join(", ")}`);
3111
- if (skipped.length > 0)
3112
- console.log(`${prefix}Skipped: ${skipped.join(", ")}`);
3113
- if (added.length === 0 && overwritten.length === 0) {
3114
- console.log(`${prefix}No accounts imported.`);
3115
- }
3287
+ printImportResults({
3288
+ added,
3289
+ overwritten,
3290
+ skipped,
3291
+ dryRun: opts.dryRun ?? false,
3292
+ noun: "account"
3293
+ });
3116
3294
  }
3117
3295
 
3118
3296
  // src/commands/apis.ts
@@ -3264,9 +3442,10 @@ ${BOLD}Examples:${RESET}
3264
3442
  $ dot chain import my-chains.json
3265
3443
  $ dot chain import my-chains.json --dry-run
3266
3444
  $ dot chain import my-chains.json --overwrite
3445
+ $ dot chain import my-chains.json --no-metadata
3267
3446
  `.trimStart();
3268
3447
  function registerChainCommands(cli) {
3269
- cli.command("chain [action] [...names]", "Manage chains (add, remove, update, list, export, import)").alias("chains").option("--all", "Update/export all configured chains").option("--relay <name>", "Parent relay chain for this parachain").option("--parachain-id <id>", "Parachain ID (auto-detected if omitted with --relay)").option("--file <path>", "Output/input file for export/import").option("--overwrite", "Overwrite existing chains on import").option("--dry-run", "Preview import without applying changes").action(async (action, names, opts) => {
3448
+ cli.command("chain [action] [...names]", "Manage chains (add, remove, update, list, export, import)").alias("chains").option("--all", "Update/export all configured chains").option("--relay <name>", "Parent relay chain for this parachain").option("--parachain-id <id>", "Parachain ID (auto-detected if omitted with --relay)").option("--file <path>", "Output/input file for export/import").option("--overwrite", "Overwrite existing chains on import").option("--dry-run", "Preview import without applying changes").option("--no-metadata", "Skip automatic metadata fetch after import").action(async (action, names, opts) => {
3270
3449
  if (!action) {
3271
3450
  if (process.argv[2] === "chains")
3272
3451
  return chainList(opts);
@@ -3476,7 +3655,17 @@ async function chainUpdate(name, opts) {
3476
3655
  }
3477
3656
  async function chainUpdateAll(config) {
3478
3657
  const chainNames = Object.keys(config.chains).sort();
3479
- process.stderr.write(`Updating metadata for ${chainNames.length} chains...
3658
+ const failed = await updateChainsMetadata(config, chainNames);
3659
+ if (failed > 0) {
3660
+ console.error(`
3661
+ ${failed} of ${chainNames.length} chains failed to update.`);
3662
+ process.exit(1);
3663
+ }
3664
+ }
3665
+ async function updateChainsMetadata(config, chainNames) {
3666
+ if (chainNames.length === 0)
3667
+ return 0;
3668
+ process.stderr.write(`Updating metadata for ${chainNames.length} chain(s)...
3480
3669
 
3481
3670
  `);
3482
3671
  const results = await Promise.allSettled(chainNames.map(async (chainName) => {
@@ -3497,12 +3686,7 @@ async function chainUpdateAll(config) {
3497
3686
  console.log(` ${RED}✗${RESET} ${name}${DIM} — ${result.reason?.message ?? "unknown error"}${RESET}`);
3498
3687
  }
3499
3688
  }
3500
- const failed = results.filter((r) => r.status === "rejected").length;
3501
- if (failed > 0) {
3502
- console.error(`
3503
- ${failed} of ${chainNames.length} chains failed to update.`);
3504
- process.exit(1);
3505
- }
3689
+ return results.filter((r) => r.status === "rejected").length;
3506
3690
  }
3507
3691
  function isBuiltinModified(name, config) {
3508
3692
  const defaultRpc = DEFAULT_CONFIG.chains[name]?.rpc;
@@ -3561,10 +3745,14 @@ async function chainExport(names, opts) {
3561
3745
  async function chainImport(filePath, opts) {
3562
3746
  const inputPath = filePath ?? opts.file;
3563
3747
  let raw;
3564
- if (inputPath && inputPath !== "-") {
3565
- raw = await readFile4(inputPath, "utf-8");
3566
- } else {
3748
+ if (!inputPath || inputPath === "-") {
3749
+ if (process.stdin.isTTY) {
3750
+ console.log(CHAIN_HELP);
3751
+ return;
3752
+ }
3567
3753
  raw = await readStdin2();
3754
+ } else {
3755
+ raw = await readFile4(inputPath, "utf-8");
3568
3756
  }
3569
3757
  let importData;
3570
3758
  try {
@@ -3618,18 +3806,16 @@ async function chainImport(filePath, opts) {
3618
3806
  }));
3619
3807
  return;
3620
3808
  }
3621
- const prefix = opts.dryRun ? "(dry run) " : "";
3622
- if (added.length > 0)
3623
- console.log(`${prefix}Added: ${added.join(", ")}`);
3624
- if (overwritten.length > 0)
3625
- console.log(`${prefix}Overwritten: ${overwritten.join(", ")}`);
3626
- if (skipped.length > 0)
3627
- console.log(`${prefix}Skipped: ${skipped.join(", ")}`);
3628
- if (added.length === 0 && overwritten.length === 0) {
3629
- console.log(`${prefix}No chains imported.`);
3630
- } else if (!opts.dryRun) {
3631
- console.error(`
3632
- Run "dot chain update --all" to fetch metadata for imported chains.`);
3809
+ printImportResults({
3810
+ added,
3811
+ overwritten,
3812
+ skipped,
3813
+ dryRun: opts.dryRun ?? false,
3814
+ noun: "chain"
3815
+ });
3816
+ if (!opts.dryRun && opts.metadata !== false && (added.length > 0 || overwritten.length > 0)) {
3817
+ console.log();
3818
+ await updateChainsMetadata(config, [...added, ...overwritten]);
3633
3819
  }
3634
3820
  }
3635
3821
 
@@ -4373,6 +4559,61 @@ async function showItemHelp2(category, target, opts) {
4373
4559
  }
4374
4560
  }
4375
4561
  }
4562
+ async function handleExtensions(target, opts) {
4563
+ const config = await loadConfig();
4564
+ const { name: chainName, chain: chainConfig } = resolveChain(config, opts.chain);
4565
+ const meta = await loadMeta2(chainName, chainConfig, opts.rpc);
4566
+ if (!target) {
4567
+ const extensions = getSignedExtensions(meta).map((e) => describeSignedExtension(meta, e)).sort((a, b) => a.identifier.localeCompare(b.identifier));
4568
+ if (isJsonOutput(opts)) {
4569
+ console.log(formatJson({
4570
+ chain: chainName,
4571
+ extensions: extensions.map((e) => ({
4572
+ identifier: e.identifier,
4573
+ valueType: e.valueType,
4574
+ additionalSignedType: e.additionalSignedType,
4575
+ isBuiltin: e.isBuiltin
4576
+ }))
4577
+ }));
4578
+ return;
4579
+ }
4580
+ printHeading(`Transaction extensions on ${chainName} (${extensions.length})`);
4581
+ for (const e of extensions) {
4582
+ const tag = e.isBuiltin ? `${DIM}[builtin]${RESET}` : `${CYAN}[custom]${RESET}`;
4583
+ printItem(e.identifier, `${e.valueType} ${tag}`);
4584
+ }
4585
+ console.log();
4586
+ return;
4587
+ }
4588
+ const info = findSignedExtension(meta, target);
4589
+ if (!info) {
4590
+ const names = getSignedExtensionNames(meta);
4591
+ throw new Error(suggestMessage("transaction extension", target, names));
4592
+ }
4593
+ const described = describeSignedExtension(meta, info);
4594
+ if (isJsonOutput(opts)) {
4595
+ console.log(formatJson({
4596
+ chain: chainName,
4597
+ identifier: described.identifier,
4598
+ valueType: described.valueType,
4599
+ additionalSignedType: described.additionalSignedType,
4600
+ valueTypeId: described.valueTypeId,
4601
+ additionalSignedTypeId: described.additionalSignedTypeId,
4602
+ isBuiltin: described.isBuiltin
4603
+ }));
4604
+ return;
4605
+ }
4606
+ printHeading(`${described.identifier} (Transaction Extension)`);
4607
+ console.log(` ${BOLD}Value type:${RESET} ${described.valueType}`);
4608
+ console.log(` ${BOLD}AdditionalSigned:${RESET} ${described.additionalSignedType}`);
4609
+ console.log(` ${BOLD}Handled by:${RESET} ${described.isBuiltin ? "polkadot-api (builtin)" : "user (custom — provide via --ext)"}`);
4610
+ if (!described.isBuiltin) {
4611
+ console.log();
4612
+ console.log(`${BOLD}Usage:${RESET}`);
4613
+ console.log(` dot ${chainName}.tx.<Pallet>.<Call> --from <acc> --ext '{"${described.identifier}":{"value":<v>}}'`);
4614
+ }
4615
+ console.log();
4616
+ }
4376
4617
 
4377
4618
  // src/commands/hash.ts
4378
4619
  init_hash();
@@ -5399,7 +5640,7 @@ async function handleTx(target, args, opts) {
5399
5640
  const at = parseAtOption(opts.at);
5400
5641
  if (!decodeOnly || opts.unsigned) {
5401
5642
  const userExtOverrides = parseExtOption(opts.ext);
5402
- const skipBuiltins = asset !== undefined ? new Set([...PAPI_BUILTIN_EXTENSIONS2].filter((e) => e !== "ChargeAssetTxPayment")) : PAPI_BUILTIN_EXTENSIONS2;
5643
+ const skipBuiltins = asset !== undefined ? new Set([...PAPI_BUILTIN_EXTENSIONS].filter((e) => e !== "ChargeAssetTxPayment")) : PAPI_BUILTIN_EXTENSIONS;
5403
5644
  if (asset !== undefined) {
5404
5645
  userExtOverrides.ChargeAssetTxPayment ??= {
5405
5646
  value: { tip: tip ?? 0n, asset_id: asset }
@@ -6330,20 +6571,6 @@ function parsePrimitive2(prim, arg) {
6330
6571
  return parseValue(arg);
6331
6572
  }
6332
6573
  }
6333
- var PAPI_BUILTIN_EXTENSIONS2 = new Set([
6334
- "CheckNonZeroSender",
6335
- "CheckSpecVersion",
6336
- "CheckTxVersion",
6337
- "CheckGenesis",
6338
- "CheckMortality",
6339
- "CheckNonce",
6340
- "CheckWeight",
6341
- "ChargeTransactionPayment",
6342
- "ChargeAssetTxPayment",
6343
- "CheckMetadataHash",
6344
- "StorageWeightReclaim",
6345
- "PrevalidateAttests"
6346
- ]);
6347
6574
  function parseExtOption(ext) {
6348
6575
  if (!ext)
6349
6576
  return {};
@@ -6361,7 +6588,7 @@ function parseExtOption(ext) {
6361
6588
  }
6362
6589
  }
6363
6590
  var NO_DEFAULT2 = Symbol("no-default");
6364
- function buildCustomSignedExtensions(meta, userOverrides, builtins = PAPI_BUILTIN_EXTENSIONS2) {
6591
+ function buildCustomSignedExtensions(meta, userOverrides, builtins = PAPI_BUILTIN_EXTENSIONS) {
6365
6592
  const result = {};
6366
6593
  const extensions = getSignedExtensions(meta);
6367
6594
  for (const ext of extensions) {
@@ -6698,9 +6925,13 @@ init_types();
6698
6925
  import { access as access3, mkdir as mkdir3, readFile as readFile7, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
6699
6926
  import { homedir as homedir2 } from "node:os";
6700
6927
  import { join as join3 } from "node:path";
6701
- var DOT_DIR2 = join3(homedir2(), ".polkadot");
6702
- var CONFIG_PATH2 = join3(DOT_DIR2, "config.json");
6703
- var CHAINS_DIR2 = join3(DOT_DIR2, "chains");
6928
+ function getConfigDir2() {
6929
+ const override = process.env.DOT_HOME;
6930
+ return override && override.length > 0 ? override : join3(homedir2(), ".polkadot");
6931
+ }
6932
+ function getConfigPath2() {
6933
+ return join3(getConfigDir2(), "config.json");
6934
+ }
6704
6935
  async function ensureDir3(dir) {
6705
6936
  await mkdir3(dir, { recursive: true });
6706
6937
  }
@@ -6713,9 +6944,10 @@ async function fileExists3(path) {
6713
6944
  }
6714
6945
  }
6715
6946
  async function loadConfig2() {
6716
- await ensureDir3(DOT_DIR2);
6717
- if (await fileExists3(CONFIG_PATH2)) {
6718
- const saved = JSON.parse(await readFile7(CONFIG_PATH2, "utf-8"));
6947
+ await ensureDir3(getConfigDir2());
6948
+ const configPath = getConfigPath2();
6949
+ if (await fileExists3(configPath)) {
6950
+ const saved = JSON.parse(await readFile7(configPath, "utf-8"));
6719
6951
  const chains = {};
6720
6952
  for (const [name, defaultConfig] of Object.entries(DEFAULT_CONFIG.chains)) {
6721
6953
  chains[name] = saved.chains[name] ? { ...defaultConfig, ...saved.chains[name] } : defaultConfig;
@@ -6731,8 +6963,8 @@ async function loadConfig2() {
6731
6963
  return DEFAULT_CONFIG;
6732
6964
  }
6733
6965
  async function saveConfig2(config) {
6734
- await ensureDir3(DOT_DIR2);
6735
- await writeFile5(CONFIG_PATH2, `${JSON.stringify(config, null, 2)}
6966
+ await ensureDir3(getConfigDir2());
6967
+ await writeFile5(getConfigPath2(), `${JSON.stringify(config, null, 2)}
6736
6968
  `);
6737
6969
  }
6738
6970
 
@@ -7012,7 +7244,10 @@ var CATEGORY_ALIASES = {
7012
7244
  errors: "errors",
7013
7245
  error: "errors",
7014
7246
  apis: "apis",
7015
- api: "apis"
7247
+ api: "apis",
7248
+ extensions: "extensions",
7249
+ extension: "extensions",
7250
+ ext: "extensions"
7016
7251
  };
7017
7252
  function matchCategory(segment) {
7018
7253
  return CATEGORY_ALIASES[segment.toLowerCase()];
@@ -7027,7 +7262,7 @@ function parseDotPath(input, knownChains = []) {
7027
7262
  const cat = matchCategory(parts[0]);
7028
7263
  if (cat)
7029
7264
  return { category: cat };
7030
- throw new Error(`Unknown command "${parts[0]}". Expected a category (query, tx, const, events, errors, apis) or a named command.`);
7265
+ throw new Error(`Unknown command "${parts[0]}". Expected a category (query, tx, const, events, errors, apis, extensions) or a named command.`);
7031
7266
  }
7032
7267
  case 2: {
7033
7268
  const cat = matchCategory(parts[0]);
@@ -7108,6 +7343,7 @@ if (process.argv[2] === "__complete") {
7108
7343
  console.log(" events List or inspect pallet events");
7109
7344
  console.log(" errors List or inspect pallet errors");
7110
7345
  console.log(" apis Browse and call runtime APIs");
7346
+ console.log(" extensions List transaction extensions on a chain");
7111
7347
  console.log();
7112
7348
  console.log("Examples:");
7113
7349
  console.log(" dot polkadot.query.System.Account <addr> Query a storage item");
@@ -7117,6 +7353,8 @@ if (process.argv[2] === "__complete") {
7117
7353
  console.log(" dot polkadot.const.Balances.ExistentialDeposit");
7118
7354
  console.log(" dot polkadot.events.Balances List events in Balances");
7119
7355
  console.log(" dot polkadot.apis.Core.version Call a runtime API");
7356
+ console.log(" dot polkadot.extensions List transaction extensions");
7357
+ console.log(" dot polkadot.extensions.CheckMortality Inspect one extension");
7120
7358
  console.log(" dot query.System.Number --chain polkadot --chain flag form");
7121
7359
  console.log(" dot ./transfer.yaml --from alice Run from file (chain in YAML)");
7122
7360
  console.log(" dot tx.0x1f0003... --to-yaml --chain polkadot Decode hex call to YAML");
@@ -7283,6 +7521,14 @@ if (process.argv[2] === "__complete") {
7283
7521
  case "apis":
7284
7522
  await handleApis2(target, args, handlerOpts);
7285
7523
  break;
7524
+ case "extensions": {
7525
+ if (parsed.item) {
7526
+ const suggestion = parsed.chain ? `dot ${parsed.chain}.extensions.${parsed.pallet}` : opts.chain ? `dot extensions.${parsed.pallet} --chain ${opts.chain}` : `dot extensions.${parsed.pallet} --chain <chain>`;
7527
+ throw new CliError2(`Transaction extensions have no sub-items. Try "${suggestion}".`);
7528
+ }
7529
+ await handleExtensions(parsed.pallet, handlerOpts);
7530
+ break;
7531
+ }
7286
7532
  }
7287
7533
  });
7288
7534
  cli.option("--help, -h", "Display this message");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "CLI tool for querying Polkadot-ecosystem on-chain state",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,6 +43,7 @@
43
43
  "@polkadot-api/view-builder": "^0.5.1",
44
44
  "@polkadot-labs/hdkd": "^0.0.28",
45
45
  "@polkadot-labs/hdkd-helpers": "^0.0.29",
46
+ "@scure/sr25519": "^1.0.0",
46
47
  "cac": "^6.7.14",
47
48
  "polkadot-api": "^2.0.1",
48
49
  "verifiablejs": "^1.2.0",