polkadot-cli 1.15.0 → 1.16.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 +50 -1
  2. package/dist/cli.mjs +246 -26
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -20,6 +20,8 @@ Ships with Polkadot and all system parachains preconfigured with multiple fallba
20
20
  - ✅ Account management — BIP39 mnemonics, derivation paths, env-backed secrets, watch-only, dev accounts
21
21
  - ✅ Named address resolution across all commands
22
22
  - ✅ Runtime API calls — `dot polkadot.apis.Core.version`
23
+ - ✅ Full-metadata dump — `dot metadata <chain>` emits one JSON blob with pallets, runtime APIs, and transaction extensions (or raw SCALE bytes via `--raw`)
24
+ - ✅ Stale-metadata detection — when a tx or query fails because the runtime upgraded, the CLI tells you exactly which `dot chain update` to run
23
25
  - ✅ Chain topology — relay/parachain hierarchy with tree display and auto-detection
24
26
  - ✅ Batteries included — all system parachains and testnets already setup to be used
25
27
  - ✅ File-based commands — run any command from a YAML/JSON file with variable substitution
@@ -577,6 +579,32 @@ The pallet listing view shows type information inline so you can understand item
577
579
 
578
580
  Documentation from the runtime metadata is shown on an indented line below each item. The detail view (`dot inspect Balances.transfer_allow_death`) shows the full argument signature and complete documentation text. Use call inspection to discover argument names, types, and docs before constructing `dot tx` commands.
579
581
 
582
+ ### Dump full metadata
583
+
584
+ `dot metadata <chain>` fetches the chain's runtime metadata and prints **everything** as one JSON blob — pallets (with calls, events, errors, storage, constants), runtime APIs, and transaction extensions, headed by a runtime fingerprint (`specVersion`, `transactionVersion`, `codeHash`, etc.). Useful for feeding LLM agents or pipelines that want a single source of truth instead of walking `dot inspect` piecemeal.
585
+
586
+ ```bash
587
+ # Decoded JSON — fetches fresh from the chain
588
+ dot metadata polkadot
589
+
590
+ # SCALE-encoded metadata bytes as a single 0x… hex line (for tools that re-parse)
591
+ dot metadata polkadot --raw
592
+
593
+ # Skip the network round-trip and use cached metadata
594
+ dot metadata polkadot --cached
595
+
596
+ # Override the RPC endpoint
597
+ dot metadata polkadot --rpc wss://rpc.example.com
598
+
599
+ # Slice with jq — list call names in a pallet
600
+ dot metadata polkadot | jq '.pallets[] | select(.name=="Balances") | .calls[].name'
601
+
602
+ # All transaction extension identifiers
603
+ dot metadata polkadot | jq '.transactionExtensions[].identifier'
604
+ ```
605
+
606
+ The decoded JSON is structured-only (no colorization), so it's safe to redirect to a file or pipe into other tools. The default fetches fresh from the RPC and atomically updates the local metadata cache and runtime-fingerprint sidecar — so subsequent commands benefit from the freshest possible metadata.
607
+
580
608
  ### Runtime APIs
581
609
 
582
610
  Browse and call Substrate runtime APIs. These are top-level APIs exposed by the runtime (e.g. `Core`, `AccountNonceApi`, `TransactionPaymentApi`), accessed as `dot <chain>.apis.ApiName.method`.
@@ -848,6 +876,22 @@ dot tx.Balances.transferKeepAlive 5FHneW46... 999999999999999999 --from alice --
848
876
  echo $? # 1
849
877
  ```
850
878
 
879
+ #### Stale metadata detection
880
+
881
+ When a `tx`, `--dry-run`, or `query` fails with an error that smells like stale metadata — a runtime wasm trap, a SCALE codec/decode error, or a fee-estimation panic — the CLI compares your locally cached metadata's runtime fingerprint against the live chain. If it has changed, the CLI appends a one-line suggestion telling you exactly which command to run:
882
+
883
+ ```
884
+ Error: The runtime rejected this transaction in the runtime's validate_transaction step.
885
+ Cause: a runtime invariant failed — typically the call's arguments are out of range, …
886
+
887
+ ⚠ Local metadata for "paseo-people-next" is out of date (spec 1018 → 1020).
888
+ Run: dot chain update paseo-people-next
889
+ ```
890
+
891
+ The fingerprint includes the runtime code hash, so the check also catches local-node restarts where the wasm changed but `specVersion` was kept the same. No automatic refetch happens — the original error still propagates with a non-zero exit code, you just get an actionable suggestion.
892
+
893
+ The check only fires on suspected-stale errors, so the happy path pays no extra RPC. Set `DOT_TRUST_CACHED_METADATA=1` to disable the check entirely (e.g. for CI loops where you've just refreshed manually).
894
+
851
895
  #### Argument parsing errors
852
896
 
853
897
  When a call argument is invalid, the CLI shows a contextual error message with the argument name, the expected type, and a hint:
@@ -1450,11 +1494,16 @@ Config and metadata caches live in `~/.polkadot/` by default:
1450
1494
  ├── update-check.json # cached update check result
1451
1495
  └── chains/
1452
1496
  └── polkadot/
1453
- └── metadata.bin # cached SCALE-encoded metadata
1497
+ ├── metadata.bin # cached SCALE-encoded metadata
1498
+ └── metadata.fingerprint.json # runtime fingerprint (specVersion, codeHash, …) for stale-metadata detection
1454
1499
  ```
1455
1500
 
1456
1501
  > **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.
1457
1502
 
1503
+ ### `DOT_TRUST_CACHED_METADATA` — skip the staleness check
1504
+
1505
+ Set `DOT_TRUST_CACHED_METADATA=1` to disable the post-failure stale-metadata check on `dot tx`, `dot tx --dry-run`, and `dot query`. When set, errors propagate exactly as the runtime / RPC reported them, with no extra `state_getRuntimeVersion` / `state_getStorageHash` round-trip. Useful in CI loops where you've just refreshed metadata manually and don't want the overhead.
1506
+
1458
1507
  ### `DOT_HOME` — redirect the config directory
1459
1508
 
1460
1509
  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.
package/dist/cli.mjs CHANGED
@@ -16,7 +16,31 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/utils/errors.ts
19
- var CliError, ConnectionError, MetadataError;
19
+ function isLikelyStaleMetadataError(err) {
20
+ const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
21
+ if (!msg)
22
+ return false;
23
+ return STALE_METADATA_PATTERNS.some((re) => re.test(msg));
24
+ }
25
+ function formatRuntimeError(err) {
26
+ const msg = err instanceof Error ? err.message : String(err);
27
+ if (/wasm trap|wasm `?unreachable`? instruction|Execution aborted due to trap/i.test(msg)) {
28
+ const frames = Array.from(msg.matchAll(/\.wasm!([A-Za-z_]\w+)/g)).map((m) => m[1]);
29
+ const fn = [...frames].reverse().find((name) => !/^(?:__rustc|core::panicking|rust_begin_unwind|panic_fmt)/.test(name));
30
+ const where = fn?.includes("validate_transaction") ? "the runtime's validate_transaction step" : fn ? `runtime function ${fn}` : "the runtime";
31
+ return [
32
+ `The runtime rejected this transaction in ${where}.`,
33
+ " Cause: a runtime invariant failed — typically the call's arguments are out of range, reference an unknown id, or violate a precondition.",
34
+ " Tip: re-check the arguments and the signing account's permissions; --dry-run will surface the same error before signing."
35
+ ].join(`
36
+ `);
37
+ }
38
+ if (/Invalid Transaction/i.test(msg)) {
39
+ return `Transaction rejected as invalid by the runtime: ${msg}`;
40
+ }
41
+ return msg;
42
+ }
43
+ var CliError, ConnectionError, MetadataError, STALE_METADATA_PATTERNS;
20
44
  var init_errors = __esm(() => {
21
45
  CliError = class CliError extends Error {
22
46
  constructor(message) {
@@ -36,8 +60,28 @@ var init_errors = __esm(() => {
36
60
  this.name = "MetadataError";
37
61
  }
38
62
  };
63
+ STALE_METADATA_PATTERNS = [
64
+ /wasm trap/i,
65
+ /wasm `?unreachable`? instruction/i,
66
+ /Execution aborted due to trap/i,
67
+ /codec/i,
68
+ /decod(e|ing)/i,
69
+ /Lookup failed/i,
70
+ /metadata.*mismatch/i
71
+ ];
39
72
  });
40
73
 
74
+ // src/utils/runtime-fingerprint.ts
75
+ function fingerprintsMatch(a, b) {
76
+ return a.codeHash === b.codeHash && a.specVersion === b.specVersion && a.transactionVersion === b.transactionVersion;
77
+ }
78
+ function isRuntimeFingerprint(value) {
79
+ if (!value || typeof value !== "object")
80
+ return false;
81
+ const v = value;
82
+ return typeof v.specName === "string" && typeof v.specVersion === "number" && typeof v.transactionVersion === "number" && typeof v.implName === "string" && typeof v.implVersion === "number" && typeof v.authoringVersion === "number" && typeof v.codeHash === "string" && typeof v.fetchedAt === "string";
83
+ }
84
+
41
85
  // src/config/types.ts
42
86
  function primaryRpc(rpc) {
43
87
  return Array.isArray(rpc) ? rpc[0] : rpc;
@@ -193,6 +237,9 @@ function getChainDir(chainName) {
193
237
  function getMetadataPath(chainName) {
194
238
  return join(getChainDir(chainName), "metadata.bin");
195
239
  }
240
+ function getMetadataFingerprintPath(chainName) {
241
+ return join(getChainDir(chainName), "metadata.fingerprint.json");
242
+ }
196
243
  function getConfigPath() {
197
244
  return join(getConfigDir(), "config.json");
198
245
  }
@@ -238,10 +285,25 @@ async function loadMetadata(chainName) {
238
285
  }
239
286
  return null;
240
287
  }
241
- async function saveMetadata(chainName, data) {
288
+ async function saveMetadata(chainName, data, fingerprint) {
242
289
  const dir = getChainDir(chainName);
243
290
  await ensureDir(dir);
244
291
  await writeFile(getMetadataPath(chainName), data);
292
+ if (fingerprint) {
293
+ await writeFile(getMetadataFingerprintPath(chainName), `${JSON.stringify(fingerprint, null, 2)}
294
+ `);
295
+ }
296
+ }
297
+ async function loadMetadataFingerprint(chainName) {
298
+ const path = getMetadataFingerprintPath(chainName);
299
+ if (!await fileExists(path))
300
+ return null;
301
+ try {
302
+ const parsed = JSON.parse(await readFile(path, "utf-8"));
303
+ return isRuntimeFingerprint(parsed) ? parsed : null;
304
+ } catch {
305
+ return null;
306
+ }
245
307
  }
246
308
  async function removeChainData(chainName) {
247
309
  const dir = getChainDir(chainName);
@@ -809,28 +871,52 @@ function parseMetadata(raw) {
809
871
  const builder = getDynamicBuilder(lookup);
810
872
  return { unified, lookup, builder, version: version2 };
811
873
  }
874
+ async function getRuntimeFingerprint(clientHandle, chainName) {
875
+ const { client } = clientHandle;
876
+ const [version2, codeHash] = await Promise.all([
877
+ withTimeout(client._request("state_getRuntimeVersion", []), chainName),
878
+ withTimeout(client._request("state_getStorageHash", ["0x3a636f6465"]), chainName)
879
+ ]);
880
+ return {
881
+ specName: version2.specName,
882
+ specVersion: version2.specVersion,
883
+ transactionVersion: version2.transactionVersion,
884
+ implName: version2.implName,
885
+ implVersion: version2.implVersion,
886
+ authoringVersion: version2.authoringVersion,
887
+ codeHash,
888
+ fetchedAt: new Date().toISOString()
889
+ };
890
+ }
812
891
  async function fetchMetadataFromChain(clientHandle, chainName) {
813
892
  const { client } = clientHandle;
893
+ let bytes;
814
894
  try {
815
895
  const hex = await withTimeout(client._request("state_call", ["Metadata_metadata_at_version", v15Arg]), chainName);
816
896
  const raw = hexToBytes(hex);
817
897
  const decoded = optionalOpaqueBytes.dec(raw);
818
898
  if (decoded !== undefined) {
819
- const bytes = new Uint8Array(decoded);
820
- await saveMetadata(chainName, bytes);
821
- return bytes;
899
+ bytes = new Uint8Array(decoded);
822
900
  }
823
901
  } catch {}
902
+ if (!bytes) {
903
+ try {
904
+ const hex = await withTimeout(client._request("state_getMetadata", []), chainName);
905
+ bytes = hexToBytes(hex);
906
+ } catch (err) {
907
+ if (err instanceof ConnectionError)
908
+ throw err;
909
+ throw new ConnectionError(`Failed to fetch metadata for "${chainName}": ${err instanceof Error ? err.message : err}. ` + "Check that the RPC endpoint is correct and reachable.");
910
+ }
911
+ }
912
+ let fingerprint;
824
913
  try {
825
- const hex = await withTimeout(client._request("state_getMetadata", []), chainName);
826
- const bytes = hexToBytes(hex);
827
- await saveMetadata(chainName, bytes);
828
- return bytes;
829
- } catch (err) {
830
- if (err instanceof ConnectionError)
831
- throw err;
832
- throw new ConnectionError(`Failed to fetch metadata for "${chainName}": ${err instanceof Error ? err.message : err}. ` + "Check that the RPC endpoint is correct and reachable.");
914
+ fingerprint = await getRuntimeFingerprint(clientHandle, chainName);
915
+ } catch {
916
+ fingerprint = undefined;
833
917
  }
918
+ await saveMetadata(chainName, bytes, fingerprint);
919
+ return bytes;
834
920
  }
835
921
  function withTimeout(promise, chainName) {
836
922
  return Promise.race([
@@ -838,6 +924,33 @@ function withTimeout(promise, chainName) {
838
924
  new Promise((_, reject) => setTimeout(() => reject(new ConnectionError(`Timed out fetching metadata for "${chainName}" after ${METADATA_TIMEOUT_MS / 1000}s. ` + "Check that the RPC endpoint is correct and reachable.")), METADATA_TIMEOUT_MS))
839
925
  ]);
840
926
  }
927
+ async function withStalenessSuggestion(chainName, clientHandle, task) {
928
+ try {
929
+ return await task();
930
+ } catch (err) {
931
+ if (process.env.DOT_TRUST_CACHED_METADATA === "1")
932
+ throw err;
933
+ if (!isLikelyStaleMetadataError(err))
934
+ throw err;
935
+ let live;
936
+ try {
937
+ live = await getRuntimeFingerprint(clientHandle, chainName);
938
+ } catch {
939
+ throw err;
940
+ }
941
+ const cached = await loadMetadataFingerprint(chainName);
942
+ if (!cached)
943
+ throw err;
944
+ if (fingerprintsMatch(cached, live))
945
+ throw err;
946
+ const original = err instanceof Error ? formatRuntimeError(err) : String(err);
947
+ const versionNote = cached.specVersion !== live.specVersion ? `spec ${cached.specVersion} → ${live.specVersion}` : `runtime code hash changed (same spec ${live.specVersion}; likely a node restart with new wasm)`;
948
+ throw new CliError(`${original}
949
+
950
+ ` + `⚠ Local metadata for "${chainName}" is out of date (${versionNote}).
951
+ ` + ` Run: dot chain update ${chainName}`);
952
+ }
953
+ }
841
954
  async function getOrFetchMetadata(chainName, clientHandle) {
842
955
  let raw = await loadMetadata(chainName);
843
956
  if (!raw) {
@@ -1251,7 +1364,10 @@ function normalizeValue(lookup, entry, value) {
1251
1364
  innerResolved = innerResolved.value;
1252
1365
  }
1253
1366
  if (innerResolved.type === "primitive" && innerResolved.value === "u8" && typeof value === "string") {
1254
- if (/^0x[0-9a-fA-F]*$/.test(value))
1367
+ const isHex = /^0x[0-9a-fA-F]*$/.test(value);
1368
+ if (resolved.type === "array" && isHex)
1369
+ return value;
1370
+ if (isHex)
1255
1371
  return Binary2.fromHex(value);
1256
1372
  return Binary2.fromText(value);
1257
1373
  }
@@ -2581,7 +2697,7 @@ var init_complete = __esm(() => {
2581
2697
  // src/cli.ts
2582
2698
  import cac from "cac";
2583
2699
  // package.json
2584
- var version = "1.15.0";
2700
+ var version = "1.16.0";
2585
2701
 
2586
2702
  // src/commands/account.ts
2587
2703
  init_accounts_store();
@@ -5078,6 +5194,61 @@ function registerInspectCommand(cli) {
5078
5194
  });
5079
5195
  }
5080
5196
 
5197
+ // src/commands/metadata.ts
5198
+ init_store();
5199
+ init_client();
5200
+ init_metadata();
5201
+ init_output();
5202
+ init_errors();
5203
+ function buildMetadataPayload(chainName, meta, fingerprint) {
5204
+ return {
5205
+ chain: chainName,
5206
+ runtime: { ...fingerprint ?? {}, metadataVersion: meta.version },
5207
+ pallets: listPallets(meta),
5208
+ runtimeApis: listRuntimeApis(meta),
5209
+ transactionExtensions: getSignedExtensions(meta)
5210
+ };
5211
+ }
5212
+ async function handleMetadata(chain, opts) {
5213
+ const config = await loadConfig();
5214
+ const { name: chainName, chain: chainConfig } = resolveChain(config, chain);
5215
+ let rawBytes;
5216
+ if (opts.cached) {
5217
+ rawBytes = await loadMetadata(chainName);
5218
+ if (!rawBytes) {
5219
+ throw new CliError(`No cached metadata for "${chainName}". Run \`dot chain update ${chainName}\` first, or omit --cached to fetch fresh.`);
5220
+ }
5221
+ } else {
5222
+ const clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
5223
+ try {
5224
+ await fetchMetadataFromChain(clientHandle, chainName);
5225
+ } finally {
5226
+ clientHandle.destroy();
5227
+ }
5228
+ rawBytes = await loadMetadata(chainName);
5229
+ if (!rawBytes) {
5230
+ throw new CliError(`Failed to load metadata for "${chainName}" after fetch.`);
5231
+ }
5232
+ }
5233
+ if (opts.raw) {
5234
+ await writeStdout(`0x${Buffer.from(rawBytes).toString("hex")}
5235
+ `);
5236
+ return;
5237
+ }
5238
+ const meta = parseMetadata(rawBytes);
5239
+ const fingerprint = await loadMetadataFingerprint(chainName);
5240
+ await writeStdout(`${formatJson(buildMetadataPayload(chainName, meta, fingerprint))}
5241
+ `);
5242
+ }
5243
+ function writeStdout(text) {
5244
+ return new Promise((resolve) => {
5245
+ process.stdout.write(text, () => resolve());
5246
+ });
5247
+ }
5248
+ function registerMetadataCommand(cli) {
5249
+ cli.command("metadata <chain>", "Fetch chain metadata (decoded JSON; --raw for SCALE hex)").option("--raw", "Print SCALE-encoded metadata bytes as hex instead of decoded JSON").option("--cached", "Use cached metadata instead of fetching fresh from the chain").option("--rpc <url>", "Override RPC endpoint(s)").action((chain, opts) => handleMetadata(chain, opts));
5250
+ }
5251
+
5081
5252
  // src/commands/parachain.ts
5082
5253
  init_accounts();
5083
5254
  init_output();
@@ -5273,13 +5444,13 @@ async function handleQuery(target, keys, opts) {
5273
5444
  console.log(`${DIM}Hint: use --dump to fetch all entries${RESET}`);
5274
5445
  return;
5275
5446
  }
5276
- const entries = await storageApi.getEntries(...parsedKeys);
5447
+ const entries = await withStalenessSuggestion(chainName, clientHandle, () => storageApi.getEntries(...parsedKeys));
5277
5448
  printResult(entries.map((e) => ({
5278
5449
  keys: e.keyArgs,
5279
5450
  value: e.value
5280
5451
  })), format);
5281
5452
  } else {
5282
- const result = await storageApi.getValue(...parsedKeys);
5453
+ const result = await withStalenessSuggestion(chainName, clientHandle, () => storageApi.getValue(...parsedKeys));
5283
5454
  printResult(result, format);
5284
5455
  }
5285
5456
  } finally {
@@ -5732,10 +5903,11 @@ async function handleTx(target, args, opts) {
5732
5903
  if (opts.dryRun) {
5733
5904
  const signerAddress = toSs58(signer.publicKey);
5734
5905
  let estimatedFees;
5906
+ let estimationError;
5735
5907
  try {
5736
- estimatedFees = String(await tx.getEstimatedFees(signer?.publicKey, txOptions));
5737
- } catch {
5738
- estimatedFees = undefined;
5908
+ estimatedFees = String(await withStalenessSuggestion(chainName, clientHandle, () => tx.getEstimatedFees(signer?.publicKey, txOptions)));
5909
+ } catch (err) {
5910
+ estimationError = err instanceof Error ? err.message : String(err);
5739
5911
  }
5740
5912
  if (isJsonOutput(opts)) {
5741
5913
  const result2 = {
@@ -5745,6 +5917,8 @@ async function handleTx(target, args, opts) {
5745
5917
  decoded: decodedStr,
5746
5918
  estimatedFees
5747
5919
  };
5920
+ if (estimationError !== undefined)
5921
+ result2.estimationError = estimationError;
5748
5922
  if (nonce !== undefined)
5749
5923
  result2.nonce = nonce;
5750
5924
  if (tip !== undefined)
@@ -5776,6 +5950,12 @@ async function handleTx(target, args, opts) {
5776
5950
  console.log(` ${BOLD}Estimated fees:${RESET} ${estimatedFees}`);
5777
5951
  } else {
5778
5952
  console.log(` ${BOLD}Estimated fees:${RESET} ${YELLOW}unable to estimate${RESET}`);
5953
+ if (estimationError) {
5954
+ const formatted = formatRuntimeError(estimationError).replace(/\n/g, `
5955
+ `);
5956
+ console.log(` ${YELLOW}⚠${RESET} ${formatted}`);
5957
+ console.log(` ${DIM}Submitting this transaction is likely to fail.${RESET}`);
5958
+ }
5779
5959
  }
5780
5960
  return;
5781
5961
  }
@@ -5795,7 +5975,7 @@ async function handleTx(target, args, opts) {
5795
5975
  }
5796
5976
  const observable = clientHandle.client.submitAndWatch(generalTx, at);
5797
5977
  if (isJsonOutput(opts)) {
5798
- const result3 = await watchTransactionJson(observable, waitLevel, { unsigned: true });
5978
+ const result3 = await withStalenessSuggestion(chainName, clientHandle, () => watchTransactionJson(observable, waitLevel, { unsigned: true }));
5799
5979
  const rpcUrl3 = primaryRpc(opts.rpc ?? chainConfig.rpc);
5800
5980
  if (result3.type === "broadcasted") {
5801
5981
  printJsonLine({ event: "broadcasted", txHash: result3.txHash });
@@ -5827,7 +6007,7 @@ async function handleTx(target, args, opts) {
5827
6007
  }
5828
6008
  return;
5829
6009
  }
5830
- const result2 = await watchTransaction(observable, waitLevel, { unsigned: true });
6010
+ const result2 = await withStalenessSuggestion(chainName, clientHandle, () => watchTransaction(observable, waitLevel, { unsigned: true }));
5831
6011
  console.log();
5832
6012
  console.log(` ${BOLD}Chain:${RESET} ${chainName}`);
5833
6013
  console.log(` ${BOLD}Type:${RESET} unsigned (bare)`);
@@ -5876,7 +6056,7 @@ async function handleTx(target, args, opts) {
5876
6056
  return;
5877
6057
  }
5878
6058
  if (isJsonOutput(opts)) {
5879
- const result2 = await watchTransactionJson(tx.signSubmitAndWatch(signer, txOptions), waitLevel);
6059
+ const result2 = await withStalenessSuggestion(chainName, clientHandle, () => watchTransactionJson(tx.signSubmitAndWatch(signer, txOptions), waitLevel));
5880
6060
  const rpcUrl2 = primaryRpc(opts.rpc ?? chainConfig.rpc);
5881
6061
  if (result2.type === "broadcasted") {
5882
6062
  printJsonLine({ event: "broadcasted", txHash: result2.txHash });
@@ -5907,7 +6087,7 @@ async function handleTx(target, args, opts) {
5907
6087
  }
5908
6088
  return;
5909
6089
  }
5910
- const result = await watchTransaction(tx.signSubmitAndWatch(signer, txOptions), waitLevel);
6090
+ const result = await withStalenessSuggestion(chainName, clientHandle, () => watchTransaction(tx.signSubmitAndWatch(signer, txOptions), waitLevel));
5911
6091
  console.log();
5912
6092
  console.log(` ${BOLD}Chain:${RESET} ${chainName}`);
5913
6093
  console.log(` ${BOLD}Call:${RESET} ${callHex}`);
@@ -6342,7 +6522,10 @@ function normalizeValue2(lookup, entry, value) {
6342
6522
  innerResolved = innerResolved.value;
6343
6523
  }
6344
6524
  if (innerResolved.type === "primitive" && innerResolved.value === "u8" && typeof value === "string") {
6345
- if (/^0x[0-9a-fA-F]*$/.test(value))
6525
+ const isHex = /^0x[0-9a-fA-F]*$/.test(value);
6526
+ if (resolved.type === "array" && isHex)
6527
+ return value;
6528
+ if (isHex)
6346
6529
  return Binary3.fromHex(value);
6347
6530
  return Binary3.fromText(value);
6348
6531
  }
@@ -7231,6 +7414,34 @@ class CliError2 extends Error {
7231
7414
  this.name = "CliError";
7232
7415
  }
7233
7416
  }
7417
+ var PAPI_CLEANUP_PATTERNS = [
7418
+ /^Not connected$/,
7419
+ /DisjointError/,
7420
+ /ChainHead.*(aborted|stopped)/i
7421
+ ];
7422
+ function isPapiCleanupError(err) {
7423
+ if (!(err instanceof Error))
7424
+ return false;
7425
+ return PAPI_CLEANUP_PATTERNS.some((re) => re.test(err.message));
7426
+ }
7427
+ function formatRuntimeError2(err) {
7428
+ const msg = err instanceof Error ? err.message : String(err);
7429
+ if (/wasm trap|wasm `?unreachable`? instruction|Execution aborted due to trap/i.test(msg)) {
7430
+ const frames = Array.from(msg.matchAll(/\.wasm!([A-Za-z_]\w+)/g)).map((m) => m[1]);
7431
+ const fn = [...frames].reverse().find((name) => !/^(?:__rustc|core::panicking|rust_begin_unwind|panic_fmt)/.test(name));
7432
+ const where = fn?.includes("validate_transaction") ? "the runtime's validate_transaction step" : fn ? `runtime function ${fn}` : "the runtime";
7433
+ return [
7434
+ `The runtime rejected this transaction in ${where}.`,
7435
+ " Cause: a runtime invariant failed — typically the call's arguments are out of range, reference an unknown id, or violate a precondition.",
7436
+ " Tip: re-check the arguments and the signing account's permissions; --dry-run will surface the same error before signing."
7437
+ ].join(`
7438
+ `);
7439
+ }
7440
+ if (/Invalid Transaction/i.test(msg)) {
7441
+ return `Transaction rejected as invalid by the runtime: ${msg}`;
7442
+ }
7443
+ return msg;
7444
+ }
7234
7445
 
7235
7446
  // src/utils/parse-dot-path.ts
7236
7447
  var CATEGORY_ALIASES = {
@@ -7353,6 +7564,7 @@ if (process.argv[2] === "__complete") {
7353
7564
  console.log(" dot polkadot.const.Balances.ExistentialDeposit");
7354
7565
  console.log(" dot polkadot.events.Balances List events in Balances");
7355
7566
  console.log(" dot polkadot.apis.Core.version Call a runtime API");
7567
+ console.log(" dot metadata polkadot Dump runtime metadata as JSON");
7356
7568
  console.log(" dot polkadot.extensions List transaction extensions");
7357
7569
  console.log(" dot polkadot.extensions.CheckMortality Inspect one extension");
7358
7570
  console.log(" dot query.System.Number --chain polkadot --chain flag form");
@@ -7363,6 +7575,7 @@ if (process.argv[2] === "__complete") {
7363
7575
  console.log();
7364
7576
  console.log("Commands:");
7365
7577
  console.log(" inspect [target] Inspect chain metadata (alias: explore)");
7578
+ console.log(" metadata <chain> Dump runtime metadata as JSON (--raw for SCALE hex)");
7366
7579
  console.log(" chain Manage chain configurations");
7367
7580
  console.log(" account Manage accounts");
7368
7581
  console.log(" hash Hash utilities");
@@ -7389,6 +7602,7 @@ if (process.argv[2] === "__complete") {
7389
7602
  cli.option("--json", "Output as JSON (shorthand for --output json)");
7390
7603
  registerChainCommands(cli);
7391
7604
  registerInspectCommand(cli);
7605
+ registerMetadataCommand(cli);
7392
7606
  registerAccountCommands(cli);
7393
7607
  registerHashCommand(cli);
7394
7608
  registerSignCommand(cli);
@@ -7545,12 +7759,18 @@ if (process.argv[2] === "__complete") {
7545
7759
  if (err instanceof CliError2) {
7546
7760
  console.error(`Error: ${err.message}`);
7547
7761
  } else if (err instanceof Error) {
7548
- console.error(`Error: ${err.message}`);
7762
+ console.error(`Error: ${formatRuntimeError2(err)}`);
7549
7763
  } else {
7550
7764
  console.error("An unexpected error occurred:", err);
7551
7765
  }
7552
7766
  return showUpdateAndExit(1);
7553
7767
  }
7768
+ process.on("unhandledRejection", (reason) => {
7769
+ if (isPapiCleanupError(reason))
7770
+ return;
7771
+ console.error(`Error: ${formatRuntimeError2(reason)}`);
7772
+ process.exit(1);
7773
+ });
7554
7774
  async function main() {
7555
7775
  try {
7556
7776
  cli.parse(process.argv, { run: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI tool for querying Polkadot-ecosystem on-chain state",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,6 +52,7 @@
52
52
  "devDependencies": {
53
53
  "@biomejs/biome": "^2.4.5",
54
54
  "@changesets/cli": "^2.29.4",
55
+ "@polkadot-api/metadata-compatibility": "^0.6.1",
55
56
  "@types/bun": "^1.3.9",
56
57
  "husky": "^9.1.7"
57
58
  }