polkadot-cli 1.14.2 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +143 -25
  2. package/dist/cli.mjs +368 -116
  3. package/package.json +3 -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
@@ -1116,7 +1251,10 @@ function normalizeValue(lookup, entry, value) {
1116
1251
  innerResolved = innerResolved.value;
1117
1252
  }
1118
1253
  if (innerResolved.type === "primitive" && innerResolved.value === "u8" && typeof value === "string") {
1119
- if (/^0x[0-9a-fA-F]*$/.test(value))
1254
+ const isHex = /^0x[0-9a-fA-F]*$/.test(value);
1255
+ if (resolved.type === "array" && isHex)
1256
+ return value;
1257
+ if (isHex)
1120
1258
  return Binary2.fromHex(value);
1121
1259
  return Binary2.fromText(value);
1122
1260
  }
@@ -1325,7 +1463,7 @@ function parsePrimitive(prim, arg) {
1325
1463
  return parseValue(arg);
1326
1464
  }
1327
1465
  }
1328
- var PAPI_BUILTIN_EXTENSIONS, NO_DEFAULT;
1466
+ var NO_DEFAULT;
1329
1467
  var init_tx = __esm(() => {
1330
1468
  init_store();
1331
1469
  init_types();
@@ -1337,20 +1475,6 @@ var init_tx = __esm(() => {
1337
1475
  init_binary_display();
1338
1476
  init_errors();
1339
1477
  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
1478
  NO_DEFAULT = Symbol("no-default");
1355
1479
  });
1356
1480
 
@@ -2046,7 +2170,7 @@ var init_focused_inspect = __esm(() => {
2046
2170
  import { blake2b as blake2b2 } from "@noble/hashes/blake2.js";
2047
2171
  import { sha256 } from "@noble/hashes/sha2.js";
2048
2172
  import { keccak_256 } from "@noble/hashes/sha3.js";
2049
- import { bytesToHex, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
2173
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
2050
2174
  function computeHash(algorithm, data) {
2051
2175
  const algo = ALGORITHMS[algorithm];
2052
2176
  if (!algo) {
@@ -2065,7 +2189,7 @@ function parseInputData(input) {
2065
2189
  return new TextEncoder().encode(input);
2066
2190
  }
2067
2191
  function toHex2(bytes) {
2068
- return `0x${bytesToHex(bytes)}`;
2192
+ return `0x${bytesToHex2(bytes)}`;
2069
2193
  }
2070
2194
  function isValidAlgorithm(name) {
2071
2195
  return name in ALGORITHMS;
@@ -2124,6 +2248,12 @@ async function loadRuntimeApiNames(_config, chainName) {
2124
2248
  methodNames: a.methods.map((m) => m.name)
2125
2249
  }));
2126
2250
  }
2251
+ async function loadExtensionIdentifiers(_config, chainName) {
2252
+ const raw = await loadMetadata(chainName);
2253
+ if (!raw)
2254
+ return null;
2255
+ return getSignedExtensionNames(parseMetadata(raw));
2256
+ }
2127
2257
  function filterPallets(pallets, category) {
2128
2258
  switch (category) {
2129
2259
  case "query":
@@ -2263,6 +2393,9 @@ async function completeDotpath(currentWord, config, knownChains, precedingWords)
2263
2393
  if (category === "apis") {
2264
2394
  return completeApisCategory(first, numComplete, endsWithDot, completeSegments, currentWord, config, chainFromFlag);
2265
2395
  }
2396
+ if (category === "extensions") {
2397
+ return completeExtensionsCategory(first, numComplete, endsWithDot, currentWord, config, chainFromFlag);
2398
+ }
2266
2399
  if (numComplete === 1 && endsWithDot) {
2267
2400
  const chainName = chainFromFlag;
2268
2401
  if (!chainName)
@@ -2319,6 +2452,9 @@ async function completeDotpath(currentWord, config, knownChains, precedingWords)
2319
2452
  if (category === "apis") {
2320
2453
  return completeApisCategory(`${first}.${completeSegments[1]}`, numComplete - 1, endsWithDot, completeSegments.slice(1), currentWord, config, chainName);
2321
2454
  }
2455
+ if (category === "extensions") {
2456
+ return completeExtensionsCategory(`${first}.${completeSegments[1]}`, numComplete - 1, endsWithDot, currentWord, config, chainName);
2457
+ }
2322
2458
  const pallets = await loadPallets(config, chainName);
2323
2459
  if (!pallets)
2324
2460
  return [];
@@ -2375,6 +2511,23 @@ async function completeApisCategory(prefix, numComplete, endsWithDot, segments,
2375
2511
  }
2376
2512
  return [];
2377
2513
  }
2514
+ async function completeExtensionsCategory(prefix, numComplete, endsWithDot, currentWord, config, chainNameOverride) {
2515
+ const chainName = chainNameOverride;
2516
+ if (!chainName)
2517
+ return [];
2518
+ const names = await loadExtensionIdentifiers(config, chainName);
2519
+ if (!names)
2520
+ return [];
2521
+ if (numComplete === 1 && endsWithDot) {
2522
+ const candidates = names.map((n) => `${prefix}.${n}`);
2523
+ return filterPrefix(candidates, currentWord.slice(0, -1));
2524
+ }
2525
+ if (numComplete === 1 && !endsWithDot) {
2526
+ const candidates = names.map((n) => `${prefix}.${n}`);
2527
+ return filterPrefix(candidates, currentWord);
2528
+ }
2529
+ return [];
2530
+ }
2378
2531
  var CATEGORIES2, CATEGORY_ALIASES2, NAMED_COMMANDS, CHAIN_SUBCOMMANDS, ACCOUNT_SUBCOMMANDS, GLOBAL_OPTIONS, TX_OPTIONS, QUERY_OPTIONS;
2379
2532
  var init_complete = __esm(() => {
2380
2533
  init_accounts_store();
@@ -2382,7 +2535,7 @@ var init_complete = __esm(() => {
2382
2535
  init_accounts();
2383
2536
  init_hash();
2384
2537
  init_metadata();
2385
- CATEGORIES2 = ["query", "tx", "const", "events", "errors", "apis"];
2538
+ CATEGORIES2 = ["query", "tx", "const", "events", "errors", "apis", "extensions"];
2386
2539
  CATEGORY_ALIASES2 = {
2387
2540
  query: "query",
2388
2541
  tx: "tx",
@@ -2394,7 +2547,10 @@ var init_complete = __esm(() => {
2394
2547
  errors: "errors",
2395
2548
  error: "errors",
2396
2549
  apis: "apis",
2397
- api: "apis"
2550
+ api: "apis",
2551
+ extensions: "extensions",
2552
+ extension: "extensions",
2553
+ ext: "extensions"
2398
2554
  };
2399
2555
  NAMED_COMMANDS = ["chain", "account", "inspect", "hash", "sign", "parachain", "completions"];
2400
2556
  CHAIN_SUBCOMMANDS = ["add", "remove", "update", "list"];
@@ -2428,7 +2584,7 @@ var init_complete = __esm(() => {
2428
2584
  // src/cli.ts
2429
2585
  import cac from "cac";
2430
2586
  // package.json
2431
- var version = "1.14.2";
2587
+ var version = "1.15.1";
2432
2588
 
2433
2589
  // src/commands/account.ts
2434
2590
  init_accounts_store();
@@ -2441,31 +2597,30 @@ ${BOLD}Usage:${RESET}
2441
2597
  $ dot account add <name> --secret <s> [--path <derivation>] Import from BIP39 mnemonic
2442
2598
  $ dot account add <name> --env <VAR> [--path <derivation>] Import account backed by env variable
2443
2599
  $ 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
2600
+ $ dot account import <file> Batch-import accounts from a file
2447
2601
  $ dot account export [names...] Export accounts to stdout
2448
2602
  $ dot account derive <source> <new-name> --path <derivation> Derive a child account
2449
- $ dot account inspect <input> [--prefix <N>] Inspect an account/address/key
2603
+ $ dot account inspect <input> [--prefix <N>] [--show-secret] Inspect an account/address/key
2450
2604
  $ dot account list List all accounts
2451
2605
  $ dot account remove|delete <name> [name2] ... Remove stored account(s)
2452
2606
 
2453
2607
  ${BOLD}Examples:${RESET}
2454
2608
  $ dot account add treasury 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
2609
+ $ dot account add treasury --secret "word1 word2 ... word12"
2610
+ $ dot account add ci-signer --env MY_SECRET --path //ci
2455
2611
  $ dot account create my-validator
2456
2612
  $ dot account create my-staking --path //staking
2457
2613
  $ 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
2614
+ $ dot account import team-accounts.json
2615
+ $ dot account import accounts.json --dry-run
2616
+ $ dot account import accounts.json --overwrite
2463
2617
  $ dot account export
2464
2618
  $ dot account export treasury my-validator
2465
2619
  $ dot account export --include-secrets --file backup.json
2466
2620
  $ dot account export --watch-only
2467
2621
  $ dot account derive treasury treasury-staking --path //staking
2468
2622
  $ dot account inspect alice
2623
+ $ dot account inspect dave --show-secret
2469
2624
  $ dot account inspect 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
2470
2625
  $ dot account inspect 0xd435...a27d --prefix 0
2471
2626
  $ dot account list
@@ -2476,7 +2631,7 @@ ${YELLOW}Note: Secrets are stored unencrypted in ~/.polkadot/accounts.json.
2476
2631
  Hex seed import (0x...) is not supported via CLI.${RESET}
2477
2632
  `.trimStart();
2478
2633
  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) => {
2634
+ 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
2635
  if (!action) {
2481
2636
  if (process.argv[2] === "accounts")
2482
2637
  return accountList(opts);
@@ -2492,9 +2647,7 @@ function registerAccountCommands(cli) {
2492
2647
  return accountImport(names[0], opts);
2493
2648
  return accountAddWatchOnly(names[0], names[1], opts);
2494
2649
  case "import":
2495
- if (opts.file)
2496
- return accountBatchImport(opts);
2497
- return accountImport(names[0], opts);
2650
+ return accountBatchImport(names[0], opts);
2498
2651
  case "export":
2499
2652
  return accountExport(names, opts);
2500
2653
  case "derive":
@@ -2893,16 +3046,19 @@ async function accountInspect(input, opts) {
2893
3046
  let name;
2894
3047
  let publicKeyHex;
2895
3048
  let bandersnatch;
3049
+ let hasSecret = false;
2896
3050
  if (isDevAccount(input)) {
2897
3051
  name = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
2898
3052
  const devAddr = getDevAddress(input);
2899
3053
  publicKeyHex = publicKeyToHex(fromSs58(devAddr));
3054
+ hasSecret = true;
2900
3055
  } else {
2901
3056
  const accountsFile = await loadAccounts();
2902
3057
  const account = findAccount(accountsFile, input);
2903
3058
  if (account) {
2904
3059
  name = account.name;
2905
3060
  bandersnatch = account.bandersnatch;
3061
+ hasSecret = account.secret !== undefined;
2906
3062
  if (account.publicKey) {
2907
3063
  publicKeyHex = account.publicKey;
2908
3064
  } else if (account.secret !== undefined && isEnvSecret(account.secret)) {
@@ -2929,12 +3085,31 @@ async function accountInspect(input, opts) {
2929
3085
  }
2930
3086
  }
2931
3087
  const ss58 = toSs58(publicKeyHex, prefix);
3088
+ let privateKeyHex;
3089
+ if (opts.showSecret) {
3090
+ if (!name) {
3091
+ console.error("--show-secret requires an account name; raw addresses and hex keys have no secret to reveal.");
3092
+ process.exit(1);
3093
+ }
3094
+ if (!hasSecret) {
3095
+ console.error(`Account "${name}" is watch-only (no secret). Cannot reveal private key.`);
3096
+ process.exit(1);
3097
+ }
3098
+ try {
3099
+ privateKeyHex = bytesToHex(await resolveAccountExpandedSecret(input));
3100
+ } catch (err) {
3101
+ console.error(err.message);
3102
+ process.exit(1);
3103
+ }
3104
+ }
2932
3105
  if (isJsonOutput(opts)) {
2933
3106
  const result = { publicKey: publicKeyHex, ss58, prefix };
2934
3107
  if (name)
2935
3108
  result.name = name;
2936
3109
  if (bandersnatch && Object.keys(bandersnatch).length > 0)
2937
3110
  result.bandersnatch = bandersnatch;
3111
+ if (privateKeyHex)
3112
+ result.privateKey = privateKeyHex;
2938
3113
  console.log(formatJson(result));
2939
3114
  } else {
2940
3115
  printHeading("Account Info");
@@ -2955,6 +3130,10 @@ async function accountInspect(input, opts) {
2955
3130
  }
2956
3131
  }
2957
3132
  console.log(` ${BOLD}Prefix:${RESET} ${prefix}`);
3133
+ if (privateKeyHex) {
3134
+ console.log(` ${BOLD}Private Key:${RESET} ${privateKeyHex}`);
3135
+ console.log(` ${YELLOW}(sr25519 expanded, 64 bytes — never share)${RESET}`);
3136
+ }
2958
3137
  console.log();
2959
3138
  }
2960
3139
  }
@@ -3019,12 +3198,17 @@ async function accountExport(names, opts) {
3019
3198
  process.stdout.write(json);
3020
3199
  }
3021
3200
  }
3022
- async function accountBatchImport(opts) {
3201
+ async function accountBatchImport(filePath, opts) {
3202
+ const inputPath = filePath ?? opts.file;
3023
3203
  let raw;
3024
- if (!opts.file || opts.file === "-") {
3204
+ if (!inputPath || inputPath === "-") {
3205
+ if (process.stdin.isTTY) {
3206
+ console.log(ACCOUNT_HELP);
3207
+ return;
3208
+ }
3025
3209
  raw = await readStdin();
3026
3210
  } else {
3027
- raw = await readFile3(opts.file, "utf-8");
3211
+ raw = await readFile3(inputPath, "utf-8");
3028
3212
  }
3029
3213
  let importData;
3030
3214
  try {
@@ -3103,16 +3287,13 @@ async function accountBatchImport(opts) {
3103
3287
  }));
3104
3288
  return;
3105
3289
  }
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
- }
3290
+ printImportResults({
3291
+ added,
3292
+ overwritten,
3293
+ skipped,
3294
+ dryRun: opts.dryRun ?? false,
3295
+ noun: "account"
3296
+ });
3116
3297
  }
3117
3298
 
3118
3299
  // src/commands/apis.ts
@@ -3264,9 +3445,10 @@ ${BOLD}Examples:${RESET}
3264
3445
  $ dot chain import my-chains.json
3265
3446
  $ dot chain import my-chains.json --dry-run
3266
3447
  $ dot chain import my-chains.json --overwrite
3448
+ $ dot chain import my-chains.json --no-metadata
3267
3449
  `.trimStart();
3268
3450
  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) => {
3451
+ 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
3452
  if (!action) {
3271
3453
  if (process.argv[2] === "chains")
3272
3454
  return chainList(opts);
@@ -3476,7 +3658,17 @@ async function chainUpdate(name, opts) {
3476
3658
  }
3477
3659
  async function chainUpdateAll(config) {
3478
3660
  const chainNames = Object.keys(config.chains).sort();
3479
- process.stderr.write(`Updating metadata for ${chainNames.length} chains...
3661
+ const failed = await updateChainsMetadata(config, chainNames);
3662
+ if (failed > 0) {
3663
+ console.error(`
3664
+ ${failed} of ${chainNames.length} chains failed to update.`);
3665
+ process.exit(1);
3666
+ }
3667
+ }
3668
+ async function updateChainsMetadata(config, chainNames) {
3669
+ if (chainNames.length === 0)
3670
+ return 0;
3671
+ process.stderr.write(`Updating metadata for ${chainNames.length} chain(s)...
3480
3672
 
3481
3673
  `);
3482
3674
  const results = await Promise.allSettled(chainNames.map(async (chainName) => {
@@ -3497,12 +3689,7 @@ async function chainUpdateAll(config) {
3497
3689
  console.log(` ${RED}✗${RESET} ${name}${DIM} — ${result.reason?.message ?? "unknown error"}${RESET}`);
3498
3690
  }
3499
3691
  }
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
- }
3692
+ return results.filter((r) => r.status === "rejected").length;
3506
3693
  }
3507
3694
  function isBuiltinModified(name, config) {
3508
3695
  const defaultRpc = DEFAULT_CONFIG.chains[name]?.rpc;
@@ -3561,10 +3748,14 @@ async function chainExport(names, opts) {
3561
3748
  async function chainImport(filePath, opts) {
3562
3749
  const inputPath = filePath ?? opts.file;
3563
3750
  let raw;
3564
- if (inputPath && inputPath !== "-") {
3565
- raw = await readFile4(inputPath, "utf-8");
3566
- } else {
3751
+ if (!inputPath || inputPath === "-") {
3752
+ if (process.stdin.isTTY) {
3753
+ console.log(CHAIN_HELP);
3754
+ return;
3755
+ }
3567
3756
  raw = await readStdin2();
3757
+ } else {
3758
+ raw = await readFile4(inputPath, "utf-8");
3568
3759
  }
3569
3760
  let importData;
3570
3761
  try {
@@ -3618,18 +3809,16 @@ async function chainImport(filePath, opts) {
3618
3809
  }));
3619
3810
  return;
3620
3811
  }
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.`);
3812
+ printImportResults({
3813
+ added,
3814
+ overwritten,
3815
+ skipped,
3816
+ dryRun: opts.dryRun ?? false,
3817
+ noun: "chain"
3818
+ });
3819
+ if (!opts.dryRun && opts.metadata !== false && (added.length > 0 || overwritten.length > 0)) {
3820
+ console.log();
3821
+ await updateChainsMetadata(config, [...added, ...overwritten]);
3633
3822
  }
3634
3823
  }
3635
3824
 
@@ -4373,6 +4562,61 @@ async function showItemHelp2(category, target, opts) {
4373
4562
  }
4374
4563
  }
4375
4564
  }
4565
+ async function handleExtensions(target, opts) {
4566
+ const config = await loadConfig();
4567
+ const { name: chainName, chain: chainConfig } = resolveChain(config, opts.chain);
4568
+ const meta = await loadMeta2(chainName, chainConfig, opts.rpc);
4569
+ if (!target) {
4570
+ const extensions = getSignedExtensions(meta).map((e) => describeSignedExtension(meta, e)).sort((a, b) => a.identifier.localeCompare(b.identifier));
4571
+ if (isJsonOutput(opts)) {
4572
+ console.log(formatJson({
4573
+ chain: chainName,
4574
+ extensions: extensions.map((e) => ({
4575
+ identifier: e.identifier,
4576
+ valueType: e.valueType,
4577
+ additionalSignedType: e.additionalSignedType,
4578
+ isBuiltin: e.isBuiltin
4579
+ }))
4580
+ }));
4581
+ return;
4582
+ }
4583
+ printHeading(`Transaction extensions on ${chainName} (${extensions.length})`);
4584
+ for (const e of extensions) {
4585
+ const tag = e.isBuiltin ? `${DIM}[builtin]${RESET}` : `${CYAN}[custom]${RESET}`;
4586
+ printItem(e.identifier, `${e.valueType} ${tag}`);
4587
+ }
4588
+ console.log();
4589
+ return;
4590
+ }
4591
+ const info = findSignedExtension(meta, target);
4592
+ if (!info) {
4593
+ const names = getSignedExtensionNames(meta);
4594
+ throw new Error(suggestMessage("transaction extension", target, names));
4595
+ }
4596
+ const described = describeSignedExtension(meta, info);
4597
+ if (isJsonOutput(opts)) {
4598
+ console.log(formatJson({
4599
+ chain: chainName,
4600
+ identifier: described.identifier,
4601
+ valueType: described.valueType,
4602
+ additionalSignedType: described.additionalSignedType,
4603
+ valueTypeId: described.valueTypeId,
4604
+ additionalSignedTypeId: described.additionalSignedTypeId,
4605
+ isBuiltin: described.isBuiltin
4606
+ }));
4607
+ return;
4608
+ }
4609
+ printHeading(`${described.identifier} (Transaction Extension)`);
4610
+ console.log(` ${BOLD}Value type:${RESET} ${described.valueType}`);
4611
+ console.log(` ${BOLD}AdditionalSigned:${RESET} ${described.additionalSignedType}`);
4612
+ console.log(` ${BOLD}Handled by:${RESET} ${described.isBuiltin ? "polkadot-api (builtin)" : "user (custom — provide via --ext)"}`);
4613
+ if (!described.isBuiltin) {
4614
+ console.log();
4615
+ console.log(`${BOLD}Usage:${RESET}`);
4616
+ console.log(` dot ${chainName}.tx.<Pallet>.<Call> --from <acc> --ext '{"${described.identifier}":{"value":<v>}}'`);
4617
+ }
4618
+ console.log();
4619
+ }
4376
4620
 
4377
4621
  // src/commands/hash.ts
4378
4622
  init_hash();
@@ -5399,7 +5643,7 @@ async function handleTx(target, args, opts) {
5399
5643
  const at = parseAtOption(opts.at);
5400
5644
  if (!decodeOnly || opts.unsigned) {
5401
5645
  const userExtOverrides = parseExtOption(opts.ext);
5402
- const skipBuiltins = asset !== undefined ? new Set([...PAPI_BUILTIN_EXTENSIONS2].filter((e) => e !== "ChargeAssetTxPayment")) : PAPI_BUILTIN_EXTENSIONS2;
5646
+ const skipBuiltins = asset !== undefined ? new Set([...PAPI_BUILTIN_EXTENSIONS].filter((e) => e !== "ChargeAssetTxPayment")) : PAPI_BUILTIN_EXTENSIONS;
5403
5647
  if (asset !== undefined) {
5404
5648
  userExtOverrides.ChargeAssetTxPayment ??= {
5405
5649
  value: { tip: tip ?? 0n, asset_id: asset }
@@ -6101,7 +6345,10 @@ function normalizeValue2(lookup, entry, value) {
6101
6345
  innerResolved = innerResolved.value;
6102
6346
  }
6103
6347
  if (innerResolved.type === "primitive" && innerResolved.value === "u8" && typeof value === "string") {
6104
- if (/^0x[0-9a-fA-F]*$/.test(value))
6348
+ const isHex = /^0x[0-9a-fA-F]*$/.test(value);
6349
+ if (resolved.type === "array" && isHex)
6350
+ return value;
6351
+ if (isHex)
6105
6352
  return Binary3.fromHex(value);
6106
6353
  return Binary3.fromText(value);
6107
6354
  }
@@ -6330,20 +6577,6 @@ function parsePrimitive2(prim, arg) {
6330
6577
  return parseValue(arg);
6331
6578
  }
6332
6579
  }
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
6580
  function parseExtOption(ext) {
6348
6581
  if (!ext)
6349
6582
  return {};
@@ -6361,7 +6594,7 @@ function parseExtOption(ext) {
6361
6594
  }
6362
6595
  }
6363
6596
  var NO_DEFAULT2 = Symbol("no-default");
6364
- function buildCustomSignedExtensions(meta, userOverrides, builtins = PAPI_BUILTIN_EXTENSIONS2) {
6597
+ function buildCustomSignedExtensions(meta, userOverrides, builtins = PAPI_BUILTIN_EXTENSIONS) {
6365
6598
  const result = {};
6366
6599
  const extensions = getSignedExtensions(meta);
6367
6600
  for (const ext of extensions) {
@@ -6698,9 +6931,13 @@ init_types();
6698
6931
  import { access as access3, mkdir as mkdir3, readFile as readFile7, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
6699
6932
  import { homedir as homedir2 } from "node:os";
6700
6933
  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");
6934
+ function getConfigDir2() {
6935
+ const override = process.env.DOT_HOME;
6936
+ return override && override.length > 0 ? override : join3(homedir2(), ".polkadot");
6937
+ }
6938
+ function getConfigPath2() {
6939
+ return join3(getConfigDir2(), "config.json");
6940
+ }
6704
6941
  async function ensureDir3(dir) {
6705
6942
  await mkdir3(dir, { recursive: true });
6706
6943
  }
@@ -6713,9 +6950,10 @@ async function fileExists3(path) {
6713
6950
  }
6714
6951
  }
6715
6952
  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"));
6953
+ await ensureDir3(getConfigDir2());
6954
+ const configPath = getConfigPath2();
6955
+ if (await fileExists3(configPath)) {
6956
+ const saved = JSON.parse(await readFile7(configPath, "utf-8"));
6719
6957
  const chains = {};
6720
6958
  for (const [name, defaultConfig] of Object.entries(DEFAULT_CONFIG.chains)) {
6721
6959
  chains[name] = saved.chains[name] ? { ...defaultConfig, ...saved.chains[name] } : defaultConfig;
@@ -6731,8 +6969,8 @@ async function loadConfig2() {
6731
6969
  return DEFAULT_CONFIG;
6732
6970
  }
6733
6971
  async function saveConfig2(config) {
6734
- await ensureDir3(DOT_DIR2);
6735
- await writeFile5(CONFIG_PATH2, `${JSON.stringify(config, null, 2)}
6972
+ await ensureDir3(getConfigDir2());
6973
+ await writeFile5(getConfigPath2(), `${JSON.stringify(config, null, 2)}
6736
6974
  `);
6737
6975
  }
6738
6976
 
@@ -7012,7 +7250,10 @@ var CATEGORY_ALIASES = {
7012
7250
  errors: "errors",
7013
7251
  error: "errors",
7014
7252
  apis: "apis",
7015
- api: "apis"
7253
+ api: "apis",
7254
+ extensions: "extensions",
7255
+ extension: "extensions",
7256
+ ext: "extensions"
7016
7257
  };
7017
7258
  function matchCategory(segment) {
7018
7259
  return CATEGORY_ALIASES[segment.toLowerCase()];
@@ -7027,7 +7268,7 @@ function parseDotPath(input, knownChains = []) {
7027
7268
  const cat = matchCategory(parts[0]);
7028
7269
  if (cat)
7029
7270
  return { category: cat };
7030
- throw new Error(`Unknown command "${parts[0]}". Expected a category (query, tx, const, events, errors, apis) or a named command.`);
7271
+ throw new Error(`Unknown command "${parts[0]}". Expected a category (query, tx, const, events, errors, apis, extensions) or a named command.`);
7031
7272
  }
7032
7273
  case 2: {
7033
7274
  const cat = matchCategory(parts[0]);
@@ -7108,6 +7349,7 @@ if (process.argv[2] === "__complete") {
7108
7349
  console.log(" events List or inspect pallet events");
7109
7350
  console.log(" errors List or inspect pallet errors");
7110
7351
  console.log(" apis Browse and call runtime APIs");
7352
+ console.log(" extensions List transaction extensions on a chain");
7111
7353
  console.log();
7112
7354
  console.log("Examples:");
7113
7355
  console.log(" dot polkadot.query.System.Account <addr> Query a storage item");
@@ -7117,6 +7359,8 @@ if (process.argv[2] === "__complete") {
7117
7359
  console.log(" dot polkadot.const.Balances.ExistentialDeposit");
7118
7360
  console.log(" dot polkadot.events.Balances List events in Balances");
7119
7361
  console.log(" dot polkadot.apis.Core.version Call a runtime API");
7362
+ console.log(" dot polkadot.extensions List transaction extensions");
7363
+ console.log(" dot polkadot.extensions.CheckMortality Inspect one extension");
7120
7364
  console.log(" dot query.System.Number --chain polkadot --chain flag form");
7121
7365
  console.log(" dot ./transfer.yaml --from alice Run from file (chain in YAML)");
7122
7366
  console.log(" dot tx.0x1f0003... --to-yaml --chain polkadot Decode hex call to YAML");
@@ -7283,6 +7527,14 @@ if (process.argv[2] === "__complete") {
7283
7527
  case "apis":
7284
7528
  await handleApis2(target, args, handlerOpts);
7285
7529
  break;
7530
+ case "extensions": {
7531
+ if (parsed.item) {
7532
+ 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>`;
7533
+ throw new CliError2(`Transaction extensions have no sub-items. Try "${suggestion}".`);
7534
+ }
7535
+ await handleExtensions(parsed.pallet, handlerOpts);
7536
+ break;
7537
+ }
7286
7538
  }
7287
7539
  });
7288
7540
  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.1",
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",
@@ -51,6 +52,7 @@
51
52
  "devDependencies": {
52
53
  "@biomejs/biome": "^2.4.5",
53
54
  "@changesets/cli": "^2.29.4",
55
+ "@polkadot-api/metadata-compatibility": "^0.6.1",
54
56
  "@types/bun": "^1.3.9",
55
57
  "husky": "^9.1.7"
56
58
  }