polkadot-cli 0.4.0 → 0.6.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 +116 -36
  2. package/dist/cli.mjs +357 -51
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # polkadot-cli
2
2
 
3
- A command-line tool for interacting with Polkadot-ecosystem chains. Query storage, look up constants, inspect metadata, manage accounts, and submit extrinsics — all from your terminal.
3
+ A command-line tool for interacting with Polkadot-ecosystem chains. Manage chains and accounts, query storage, look up constants, inspect metadata, submit extrinsics, and compute hashes — all from your terminal.
4
4
 
5
5
  Ships with Polkadot as the default chain. Add any Substrate-based chain by pointing to its RPC endpoint.
6
6
 
@@ -14,6 +14,48 @@ This installs the `dot` command globally.
14
14
 
15
15
  ## Usage
16
16
 
17
+ ### Manage chains
18
+
19
+ ```bash
20
+ # Add a chain
21
+ dot chain add kusama --rpc wss://kusama-rpc.polkadot.io
22
+ dot chain add westend --light-client
23
+
24
+ # List configured chains
25
+ dot chain list
26
+
27
+ # Re-fetch metadata after a runtime upgrade
28
+ dot chain update # updates default chain
29
+ dot chain update kusama # updates a specific chain
30
+
31
+ # Set default chain
32
+ dot chain default kusama
33
+
34
+ # Remove a chain
35
+ dot chain remove westend
36
+ ```
37
+
38
+ ### Manage accounts
39
+
40
+ Dev accounts (Alice, Bob, Charlie, Dave, Eve, Ferdie) are always available for testnets. Create or import your own accounts for any chain.
41
+
42
+ ```bash
43
+ # List all accounts (dev + stored)
44
+ dot account list
45
+
46
+ # Create a new account (generates a mnemonic)
47
+ dot account create my-validator
48
+
49
+ # Import from a BIP39 mnemonic
50
+ dot account import treasury --secret "word1 word2 ... word12"
51
+
52
+ # Import from a hex seed
53
+ dot account import raw-key --secret 0xabcdef...
54
+
55
+ # Remove an account
56
+ dot account remove my-validator
57
+ ```
58
+
17
59
  ### Query storage
18
60
 
19
61
  ```bash
@@ -52,30 +94,9 @@ dot inspect System
52
94
  dot inspect System.Account
53
95
  ```
54
96
 
55
- ### Manage accounts
56
-
57
- Dev accounts (Alice, Bob, Charlie, Dave, Eve, Ferdie) are always available for testnets. Create or import your own accounts for any chain.
58
-
59
- ```bash
60
- # List all accounts (dev + stored)
61
- dot account list
62
-
63
- # Create a new account (generates a mnemonic)
64
- dot account create my-validator
65
-
66
- # Import from a BIP39 mnemonic
67
- dot account import treasury --secret "word1 word2 ... word12"
68
-
69
- # Import from a hex seed
70
- dot account import raw-key --secret 0xabcdef...
71
-
72
- # Remove an account
73
- dot account remove my-validator
74
- ```
75
-
76
97
  ### Submit extrinsics
77
98
 
78
- Build, sign, and submit transactions. Arguments are parsed from metadata the CLI knows the expected types for each call.
99
+ 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.
79
100
 
80
101
  ```bash
81
102
  # Simple remark
@@ -87,30 +108,74 @@ dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694
87
108
  # Estimate fees without submitting
88
109
  dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 1000000000000 --from alice --dry-run
89
110
 
111
+ # Submit a raw SCALE-encoded call (e.g. from a multisig proposal or another tool)
112
+ dot tx 0x0503008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 --from alice
113
+
90
114
  # Batch multiple transfers with Utility.batchAll
91
115
  dot tx Utility.batchAll '[{"type":"Balances","value":{"type":"transfer_keep_alive","value":{"dest":"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty","value":1000000000000}}},{"type":"Balances","value":{"type":"transfer_keep_alive","value":{"dest":"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y","value":2000000000000}}}]' --from alice
92
116
  ```
93
117
 
94
- ### Manage chains
118
+ #### Encode call data
119
+
120
+ Encode a call to hex without signing or submitting. Useful for preparing calls to pass to `Sudo.sudo`, multisig proposals, or governance. Works offline from cached metadata and does not require `--from`.
95
121
 
96
122
  ```bash
97
- # Add a chain
98
- dot chain add kusama --rpc wss://kusama-rpc.polkadot.io
99
- dot chain add westend --light-client
123
+ # Encode a remark call
124
+ dot tx System.remark 0xdeadbeef --encode
100
125
 
101
- # List configured chains
102
- dot chain list
126
+ # Encode a transfer (use the hex output in a batch or sudo call)
127
+ dot tx Balances.transfer_keep_alive 5FHneW46... 1000000000000 --encode
103
128
 
104
- # Re-fetch metadata after a runtime upgrade
105
- dot chain update # updates default chain
106
- dot chain update kusama # updates a specific chain
129
+ # Use encoded output with Sudo.sudo
130
+ dot tx Sudo.sudo $(dot tx System.remark 0xcafe --encode) --from alice
131
+ ```
107
132
 
108
- # Set default chain
109
- dot chain default kusama
133
+ Both dry-run and submission display the encoded call hex and a decoded human-readable form:
110
134
 
111
- # Remove a chain
112
- dot chain remove westend
113
135
  ```
136
+ Call: 0x0001076465616462656566
137
+ Decode: System.remark(remark: 0xdeadbeef)
138
+ Tx: 0xabc123...
139
+ Block: #12345678 (0xdef...)
140
+ Status: ok
141
+ ```
142
+
143
+ #### Custom signed extensions
144
+
145
+ Chains with non-standard signed extensions (e.g. `people-preview`) are auto-handled:
146
+
147
+ - `void` → empty bytes
148
+ - `Option<T>` → `None`
149
+ - enum with `Disabled` variant → `Disabled`
150
+
151
+ For manual override, use `--ext` with a JSON object:
152
+
153
+ ```bash
154
+ dot tx System.remark 0xdeadbeef --from alice --ext '{"MyExtension":{"value":"..."}}'
155
+ ```
156
+
157
+ ### Compute hashes
158
+
159
+ Compute cryptographic hashes commonly used in Substrate. Supports BLAKE2b-256, BLAKE2b-128, Keccak-256, and SHA-256.
160
+
161
+ ```bash
162
+ # Hash hex-encoded data
163
+ dot hash blake2b256 0xdeadbeef
164
+
165
+ # Hash plain text (UTF-8 encoded)
166
+ dot hash sha256 hello
167
+
168
+ # Hash file contents
169
+ dot hash keccak256 --file ./data.bin
170
+
171
+ # Read from stdin
172
+ echo -n "hello" | dot hash sha256 --stdin
173
+
174
+ # JSON output
175
+ dot hash blake2b256 0xdeadbeef --output json
176
+ ```
177
+
178
+ Run `dot hash` with no arguments to see all available algorithms.
114
179
 
115
180
  ### Global options
116
181
 
@@ -122,6 +187,21 @@ dot chain remove westend
122
187
  | `--output json` | Raw JSON output (default: pretty) |
123
188
  | `--limit <n>` | Max entries for map queries (0 = unlimited, default: 100) |
124
189
 
190
+ ## How it compares
191
+
192
+ | | polkadot-cli | @polkadot/api-cli | subxt-cli | Pop CLI |
193
+ |---|---|---|---|---|
194
+ | **Query storage** | SS58 keys, map iteration | yes (full `--ws` URL required) | yes (keys as SCALE tuples, no SS58) | — |
195
+ | **Read constants** | yes | yes | yes | — |
196
+ | **Submit extrinsics** | yes, with dry-run | yes (via `--seed`) | — | ink! contract calls only |
197
+ | **Inspect metadata** | yes | — | yes (excellent browser) | — |
198
+ | **Chain presets** | built-in aliases (`--chain kusama`) | — (manual `--ws` every call) | — | parachain templates |
199
+ | **Tx tracking + explorer links** | spinner progress, block + explorer link | basic events | — | — |
200
+
201
+ polkadot-cli aims to be the single tool for day-to-day chain interaction: storage reads, constant lookups, transaction submission, and metadata browsing with a polished terminal UX. @polkadot/api-cli covers similar ground but is in maintenance mode and requires verbose flags. subxt-cli has an excellent metadata explorer but cannot sign or submit transactions. Pop CLI targets a different workflow — scaffolding parachains and deploying ink! contracts rather than end-user chain queries.
202
+
203
+ Outside Polkadot, the closest comparable in terms of interactive UX is [near-cli-rs](https://github.com/near/near-cli-rs) (NEAR).
204
+
125
205
  ## Configuration
126
206
 
127
207
  Config and metadata caches live in `~/.polkadot/`:
package/dist/cli.mjs CHANGED
@@ -1,21 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
4
 
21
5
  // src/cli.ts
@@ -388,6 +372,38 @@ function printDocs(docs) {
388
372
  console.log(` ${DIM}${text}${RESET}`);
389
373
  }
390
374
  }
375
+ var CHECK_MARK = "✓";
376
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
377
+
378
+ class Spinner {
379
+ timer = null;
380
+ frame = 0;
381
+ start(msg) {
382
+ this.stop();
383
+ if (!isTTY) {
384
+ console.log(msg);
385
+ return;
386
+ }
387
+ process.stdout.write(`${SPINNER_FRAMES[0]} ${msg}`);
388
+ this.timer = setInterval(() => {
389
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
390
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[this.frame]} ${msg}`);
391
+ }, 80);
392
+ }
393
+ stop() {
394
+ if (this.timer !== null) {
395
+ clearInterval(this.timer);
396
+ this.timer = null;
397
+ this.frame = 0;
398
+ if (isTTY)
399
+ process.stdout.write("\r\x1B[K");
400
+ }
401
+ }
402
+ succeed(msg) {
403
+ this.stop();
404
+ console.log(`${GREEN}${CHECK_MARK}${RESET} ${msg}`);
405
+ }
406
+ }
391
407
 
392
408
  // src/commands/chain.ts
393
409
  var CHAIN_HELP = `
@@ -1069,28 +1085,61 @@ async function accountRemove(name) {
1069
1085
  // src/commands/tx.ts
1070
1086
  import { Binary } from "polkadot-api";
1071
1087
  import { getViewBuilder } from "@polkadot-api/view-builder";
1088
+
1089
+ // src/core/explorers.ts
1090
+ var pjsAppsLink = (rpc, hash) => `https://polkadot.js.org/apps/?rpc=${encodeURIComponent(rpc)}#/explorer/query/${hash}`;
1091
+ var papiLink = (rpc, hash) => `https://dev.papi.how/explorer/${hash}#networkId=custom&endpoint=${encodeURIComponent(rpc)}`;
1092
+
1093
+ // src/commands/tx.ts
1072
1094
  function registerTxCommand(cli) {
1073
- cli.command("tx [target] [...args]", "Submit an extrinsic (e.g. Balances.transferKeepAlive <dest> <amount>)").option("--from <name>", "Account to sign with (required)").option("--dry-run", "Estimate fees without submitting").option("--ext <json>", `Custom signed extension values as JSON, e.g. '{"ExtName":{"value":...}}'`).action(async (target, args, opts) => {
1074
- if (!target || !opts.from) {
1075
- console.log("Usage: dot tx <Pallet.Call|0xCallHex> [...args] --from <account> [--dry-run]");
1095
+ cli.command("tx [target] [...args]", "Submit an extrinsic (e.g. Balances.transferKeepAlive <dest> <amount>)").option("--from <name>", "Account to sign with (required)").option("--dry-run", "Estimate fees without submitting").option("--encode", "Encode call to hex without signing or submitting").option("--ext <json>", `Custom signed extension values as JSON, e.g. '{"ExtName":{"value":...}}'`).action(async (target, args, opts) => {
1096
+ if (!target) {
1097
+ console.log("Usage: dot tx <Pallet.Call|0xCallHex> [...args] --from <account> [--dry-run] [--encode]");
1076
1098
  console.log("");
1077
1099
  console.log("Examples:");
1078
1100
  console.log(" $ dot tx Balances.transferKeepAlive 5FHn... 1000000000000 --from alice");
1079
1101
  console.log(" $ dot tx System.remark 0xdeadbeef --from alice --dry-run");
1080
1102
  console.log(" $ dot tx 0x0001076465616462656566 --from alice");
1103
+ console.log(" $ dot tx Assets.force_create 4 owner true 10 --encode --chain people");
1081
1104
  return;
1082
1105
  }
1106
+ if (!opts.from && !opts.encode) {
1107
+ throw new Error("--from is required (or use --encode to output hex without signing)");
1108
+ }
1109
+ if (opts.encode && opts.dryRun) {
1110
+ throw new Error("--encode and --dry-run are mutually exclusive");
1111
+ }
1083
1112
  const isRawCall = /^0x[0-9a-fA-F]+$/.test(target);
1113
+ if (opts.encode && isRawCall) {
1114
+ throw new Error("--encode cannot be used with raw call hex (already encoded)");
1115
+ }
1084
1116
  const config = await loadConfig();
1085
1117
  const { name: chainName, chain: chainConfig } = resolveChain(config, opts.chain);
1086
- const signer = await resolveAccountSigner(opts.from);
1087
- const clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
1118
+ const signer = opts.encode ? undefined : await resolveAccountSigner(opts.from);
1119
+ let clientHandle;
1120
+ if (!opts.encode) {
1121
+ clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
1122
+ }
1088
1123
  try {
1089
- const meta = await getOrFetchMetadata(chainName, clientHandle);
1090
- const userExtOverrides = parseExtOption(opts.ext);
1091
- const customSignedExtensions = buildCustomSignedExtensions(meta, userExtOverrides);
1092
- const txOptions = Object.keys(customSignedExtensions).length > 0 ? { customSignedExtensions } : undefined;
1093
- const unsafeApi = clientHandle.client.getUnsafeApi();
1124
+ let meta;
1125
+ if (clientHandle) {
1126
+ meta = await getOrFetchMetadata(chainName, clientHandle);
1127
+ } else {
1128
+ try {
1129
+ meta = await getOrFetchMetadata(chainName);
1130
+ } catch {
1131
+ clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
1132
+ meta = await getOrFetchMetadata(chainName, clientHandle);
1133
+ }
1134
+ }
1135
+ let unsafeApi;
1136
+ let txOptions;
1137
+ if (!opts.encode) {
1138
+ const userExtOverrides = parseExtOption(opts.ext);
1139
+ const customSignedExtensions = buildCustomSignedExtensions(meta, userExtOverrides);
1140
+ txOptions = Object.keys(customSignedExtensions).length > 0 ? { customSignedExtensions } : undefined;
1141
+ unsafeApi = clientHandle.client.getUnsafeApi();
1142
+ }
1094
1143
  let tx;
1095
1144
  let callHex;
1096
1145
  if (isRawCall) {
@@ -1114,6 +1163,17 @@ function registerTxCommand(cli) {
1114
1163
  throw new Error(suggestMessage(`call in ${palletInfo.name}`, callName, callNames));
1115
1164
  }
1116
1165
  const callData = parseCallArgs(meta, palletInfo.name, callInfo.name, args);
1166
+ if (opts.encode) {
1167
+ const { codec, location } = meta.builder.buildCall(palletInfo.name, callInfo.name);
1168
+ const encodedArgs = codec.enc(callData);
1169
+ const fullCall = new Uint8Array([
1170
+ location[0],
1171
+ location[1],
1172
+ ...encodedArgs
1173
+ ]);
1174
+ console.log(Binary.fromBytes(fullCall).asHex());
1175
+ return;
1176
+ }
1117
1177
  tx = unsafeApi.tx[palletInfo.name][callInfo.name](callData);
1118
1178
  const encodedCall = await tx.getEncodedData();
1119
1179
  callHex = encodedCall.asHex();
@@ -1133,20 +1193,17 @@ function registerTxCommand(cli) {
1133
1193
  }
1134
1194
  return;
1135
1195
  }
1136
- console.log("Signing and submitting...");
1137
- const result = await tx.signAndSubmit(signer, txOptions);
1196
+ const result = await watchTransaction(tx.signSubmitAndWatch(signer, txOptions));
1138
1197
  console.log();
1139
1198
  console.log(` ${BOLD}Call:${RESET} ${callHex}`);
1140
1199
  console.log(` ${BOLD}Decode:${RESET} ${decodedStr}`);
1141
1200
  console.log(` ${BOLD}Tx:${RESET} ${result.txHash}`);
1142
- if (result.block) {
1143
- console.log(` ${BOLD}Block:${RESET} #${result.block.number} (${result.block.hash})`);
1144
- }
1145
- if (result.dispatchError) {
1201
+ console.log(` ${BOLD}Block:${RESET} #${result.block.number} (${result.block.hash})`);
1202
+ if (result.ok) {
1203
+ console.log(` ${BOLD}Status:${RESET} ${GREEN}ok${RESET}`);
1204
+ } else {
1146
1205
  console.log(` ${BOLD}Status:${RESET} ${YELLOW}dispatch error${RESET}`);
1147
1206
  console.log(` ${BOLD}Error:${RESET} ${result.dispatchError.type}${result.dispatchError.value ? ": " + JSON.stringify(result.dispatchError.value) : ""}`);
1148
- } else {
1149
- console.log(` ${BOLD}Status:${RESET} ${GREEN}ok${RESET}`);
1150
1207
  }
1151
1208
  if (result.events && result.events.length > 0) {
1152
1209
  console.log(` ${BOLD}Events:${RESET}`);
@@ -1161,9 +1218,16 @@ function registerTxCommand(cli) {
1161
1218
  }
1162
1219
  }
1163
1220
  }
1221
+ const rpcUrl = opts.rpc ?? chainConfig.rpc;
1222
+ if (rpcUrl) {
1223
+ const blockHash = result.block.hash;
1224
+ console.log(` ${BOLD}Explorer:${RESET}`);
1225
+ console.log(` ${DIM}PolkadotJS${RESET} ${pjsAppsLink(rpcUrl, blockHash)}`);
1226
+ console.log(` ${DIM}PAPI${RESET} ${papiLink(rpcUrl, blockHash)}`);
1227
+ }
1164
1228
  console.log();
1165
1229
  } finally {
1166
- clientHandle.destroy();
1230
+ clientHandle?.destroy();
1167
1231
  }
1168
1232
  });
1169
1233
  }
@@ -1286,33 +1350,33 @@ function parseCallArgs(meta, palletName, callName, args) {
1286
1350
  return;
1287
1351
  }
1288
1352
  if (variant.type === "struct") {
1289
- return parseStructArgs(meta.lookup, variant.value, args, `${palletName}.${callName}`);
1353
+ return parseStructArgs(meta, variant.value, args, `${palletName}.${callName}`);
1290
1354
  }
1291
1355
  if (variant.type === "lookupEntry") {
1292
1356
  const inner = variant.value;
1293
1357
  if (inner.type === "struct") {
1294
- return parseStructArgs(meta.lookup, inner.value, args, `${palletName}.${callName}`);
1358
+ return parseStructArgs(meta, inner.value, args, `${palletName}.${callName}`);
1295
1359
  }
1296
1360
  if (inner.type === "void")
1297
1361
  return;
1298
1362
  if (args.length !== 1) {
1299
1363
  throw new Error(`${palletName}.${callName} takes 1 argument (${describeType(meta.lookup, inner.id)}), but ${args.length} provided.`);
1300
1364
  }
1301
- return parseTypedArg(meta.lookup, inner, args[0]);
1365
+ return parseTypedArg(meta, inner, args[0]);
1302
1366
  }
1303
1367
  if (variant.type === "tuple") {
1304
1368
  const entries = variant.value;
1305
1369
  if (args.length !== entries.length) {
1306
1370
  throw new Error(`${palletName}.${callName} takes ${entries.length} arguments, but ${args.length} provided.`);
1307
1371
  }
1308
- return entries.map((entry, i) => parseTypedArg(meta.lookup, entry, args[i]));
1372
+ return entries.map((entry, i) => parseTypedArg(meta, entry, args[i]));
1309
1373
  }
1310
1374
  return args.length === 0 ? undefined : args.map(parseValue);
1311
1375
  }
1312
- function parseStructArgs(lookup, fields, args, callLabel) {
1376
+ function parseStructArgs(meta, fields, args, callLabel) {
1313
1377
  const fieldNames = Object.keys(fields);
1314
1378
  if (args.length !== fieldNames.length) {
1315
- const expected = fieldNames.map((name) => `${name}: ${describeType(lookup, fields[name].id)}`).join(", ");
1379
+ const expected = fieldNames.map((name) => `${name}: ${describeType(meta.lookup, fields[name].id)}`).join(", ");
1316
1380
  throw new Error(`${callLabel} takes ${fieldNames.length} argument(s): ${expected}
1317
1381
  ` + ` Got ${args.length} argument(s).`);
1318
1382
  }
@@ -1320,11 +1384,78 @@ function parseStructArgs(lookup, fields, args, callLabel) {
1320
1384
  for (let i = 0;i < fieldNames.length; i++) {
1321
1385
  const name = fieldNames[i];
1322
1386
  const entry = fields[name];
1323
- result[name] = parseTypedArg(lookup, entry, args[i]);
1387
+ result[name] = parseTypedArg(meta, entry, args[i]);
1324
1388
  }
1325
1389
  return result;
1326
1390
  }
1327
- function parseTypedArg(lookup, entry, arg) {
1391
+ function normalizeValue(lookup, entry, value) {
1392
+ let resolved = entry;
1393
+ while (resolved.type === "lookupEntry") {
1394
+ resolved = resolved.value;
1395
+ }
1396
+ switch (resolved.type) {
1397
+ case "enum": {
1398
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && "type" in value) {
1399
+ const enumValue = value;
1400
+ const variant = resolved.value[enumValue.type];
1401
+ if (variant) {
1402
+ let innerEntry = variant;
1403
+ while (innerEntry.type === "lookupEntry") {
1404
+ innerEntry = innerEntry.value;
1405
+ }
1406
+ let normalizedInner = enumValue.value;
1407
+ if (innerEntry.type !== "array" && innerEntry.type !== "sequence" && innerEntry.type !== "void" && Array.isArray(normalizedInner) && normalizedInner.length === 1) {
1408
+ normalizedInner = normalizedInner[0];
1409
+ }
1410
+ if (normalizedInner !== undefined && innerEntry.type !== "void") {
1411
+ normalizedInner = normalizeValue(lookup, innerEntry, normalizedInner);
1412
+ }
1413
+ return { type: enumValue.type, value: normalizedInner };
1414
+ }
1415
+ }
1416
+ return value;
1417
+ }
1418
+ case "struct": {
1419
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1420
+ const fields = resolved.value;
1421
+ const result = {};
1422
+ for (const [key, val] of Object.entries(value)) {
1423
+ if (key in fields) {
1424
+ result[key] = normalizeValue(lookup, fields[key], val);
1425
+ } else {
1426
+ result[key] = val;
1427
+ }
1428
+ }
1429
+ return result;
1430
+ }
1431
+ return value;
1432
+ }
1433
+ case "array":
1434
+ case "sequence": {
1435
+ if (Array.isArray(value)) {
1436
+ const innerEntry = resolved.value;
1437
+ return value.map((item) => normalizeValue(lookup, innerEntry, item));
1438
+ }
1439
+ return value;
1440
+ }
1441
+ case "tuple": {
1442
+ if (Array.isArray(value)) {
1443
+ const entries = resolved.value;
1444
+ return value.map((item, i) => i < entries.length ? normalizeValue(lookup, entries[i], item) : item);
1445
+ }
1446
+ return value;
1447
+ }
1448
+ case "option": {
1449
+ if (value !== null && value !== undefined) {
1450
+ return normalizeValue(lookup, resolved.value, value);
1451
+ }
1452
+ return value;
1453
+ }
1454
+ default:
1455
+ return value;
1456
+ }
1457
+ }
1458
+ function parseTypedArg(meta, entry, arg) {
1328
1459
  switch (entry.type) {
1329
1460
  case "primitive":
1330
1461
  return parsePrimitive(entry.value, arg);
@@ -1337,12 +1468,16 @@ function parseTypedArg(lookup, entry, arg) {
1337
1468
  if (arg === "null" || arg === "undefined" || arg === "none") {
1338
1469
  return;
1339
1470
  }
1340
- return parseTypedArg(lookup, entry.value, arg);
1471
+ return parseTypedArg(meta, entry.value, arg);
1341
1472
  }
1342
1473
  case "enum": {
1474
+ if (/^0x[0-9a-fA-F]+$/.test(arg) && meta.lookup.call != null && entry.id === meta.lookup.call) {
1475
+ const callCodec = meta.builder.buildDefinition(meta.lookup.call);
1476
+ return callCodec.dec(Binary.fromHex(arg).asBytes());
1477
+ }
1343
1478
  if (arg.startsWith("{")) {
1344
1479
  try {
1345
- return JSON.parse(arg);
1480
+ return normalizeValue(meta.lookup, entry, JSON.parse(arg));
1346
1481
  } catch {}
1347
1482
  }
1348
1483
  const variants = Object.keys(entry.value);
@@ -1372,7 +1507,7 @@ function parseTypedArg(lookup, entry, arg) {
1372
1507
  }
1373
1508
  if (arg.startsWith("[")) {
1374
1509
  try {
1375
- return JSON.parse(arg);
1510
+ return normalizeValue(meta.lookup, entry, JSON.parse(arg));
1376
1511
  } catch {}
1377
1512
  }
1378
1513
  if (/^0x[0-9a-fA-F]*$/.test(arg))
@@ -1382,14 +1517,14 @@ function parseTypedArg(lookup, entry, arg) {
1382
1517
  case "struct":
1383
1518
  if (arg.startsWith("{")) {
1384
1519
  try {
1385
- return JSON.parse(arg);
1520
+ return normalizeValue(meta.lookup, entry, JSON.parse(arg));
1386
1521
  } catch {}
1387
1522
  }
1388
1523
  return parseValue(arg);
1389
1524
  case "tuple":
1390
1525
  if (arg.startsWith("[")) {
1391
1526
  try {
1392
- return JSON.parse(arg);
1527
+ return normalizeValue(meta.lookup, entry, JSON.parse(arg));
1393
1528
  } catch {}
1394
1529
  }
1395
1530
  return parseValue(arg);
@@ -1489,6 +1624,170 @@ function autoDefaultForType(entry) {
1489
1624
  }
1490
1625
  return NO_DEFAULT;
1491
1626
  }
1627
+ function watchTransaction(observable) {
1628
+ const spinner = new Spinner;
1629
+ return new Promise((resolve, reject) => {
1630
+ spinner.start("Signing...");
1631
+ observable.subscribe({
1632
+ next(event) {
1633
+ switch (event.type) {
1634
+ case "signed":
1635
+ spinner.succeed("Signed");
1636
+ console.log(` ${BOLD}Tx:${RESET} ${event.txHash}`);
1637
+ spinner.start("Broadcasting...");
1638
+ break;
1639
+ case "broadcasted":
1640
+ spinner.succeed("Broadcasted");
1641
+ spinner.start("In best block...");
1642
+ break;
1643
+ case "txBestBlocksState":
1644
+ if (event.found) {
1645
+ spinner.succeed(`In best block #${event.block.number}`);
1646
+ spinner.start("Finalizing...");
1647
+ } else {
1648
+ spinner.start("In best block...");
1649
+ }
1650
+ break;
1651
+ case "finalized":
1652
+ spinner.stop();
1653
+ resolve(event);
1654
+ break;
1655
+ }
1656
+ },
1657
+ error(err) {
1658
+ spinner.stop();
1659
+ reject(err);
1660
+ }
1661
+ });
1662
+ });
1663
+ }
1664
+
1665
+ // src/core/hash.ts
1666
+ import { blake2b } from "@noble/hashes/blake2.js";
1667
+ import { keccak_256 } from "@noble/hashes/sha3.js";
1668
+ import { sha256 } from "@noble/hashes/sha2.js";
1669
+ import { bytesToHex, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
1670
+ var ALGORITHMS = {
1671
+ blake2b256: {
1672
+ compute: (data) => blake2b(data, { dkLen: 32 }),
1673
+ outputLen: 32,
1674
+ description: "BLAKE2b with 256-bit output"
1675
+ },
1676
+ blake2b128: {
1677
+ compute: (data) => blake2b(data, { dkLen: 16 }),
1678
+ outputLen: 16,
1679
+ description: "BLAKE2b with 128-bit output"
1680
+ },
1681
+ keccak256: {
1682
+ compute: (data) => keccak_256(data),
1683
+ outputLen: 32,
1684
+ description: "Keccak-256 (Ethereum-compatible)"
1685
+ },
1686
+ sha256: {
1687
+ compute: (data) => sha256(data),
1688
+ outputLen: 32,
1689
+ description: "SHA-256"
1690
+ }
1691
+ };
1692
+ function computeHash(algorithm, data) {
1693
+ const algo = ALGORITHMS[algorithm];
1694
+ if (!algo) {
1695
+ throw new Error(`Unknown algorithm: ${algorithm}`);
1696
+ }
1697
+ return algo.compute(data);
1698
+ }
1699
+ function parseInputData(input) {
1700
+ if (input.startsWith("0x")) {
1701
+ const hex = input.slice(2);
1702
+ if (hex.length % 2 !== 0) {
1703
+ throw new Error(`Invalid hex input: odd number of characters`);
1704
+ }
1705
+ return hexToBytes2(hex);
1706
+ }
1707
+ return new TextEncoder().encode(input);
1708
+ }
1709
+ function toHex(bytes) {
1710
+ return "0x" + bytesToHex(bytes);
1711
+ }
1712
+ function isValidAlgorithm(name) {
1713
+ return name in ALGORITHMS;
1714
+ }
1715
+ function getAlgorithmNames() {
1716
+ return Object.keys(ALGORITHMS);
1717
+ }
1718
+
1719
+ // src/commands/hash.ts
1720
+ async function resolveInput(data, opts) {
1721
+ const sources = [data !== undefined, !!opts.file, !!opts.stdin].filter(Boolean).length;
1722
+ if (sources > 1) {
1723
+ throw new CliError("Provide only one of: inline data, --file, or --stdin");
1724
+ }
1725
+ if (sources === 0) {
1726
+ throw new CliError("No input provided. Pass data as argument, or use --file or --stdin");
1727
+ }
1728
+ if (opts.file) {
1729
+ const buf = await Bun.file(opts.file).arrayBuffer();
1730
+ return new Uint8Array(buf);
1731
+ }
1732
+ if (opts.stdin) {
1733
+ const reader = Bun.stdin.stream().getReader();
1734
+ const chunks = [];
1735
+ while (true) {
1736
+ const { done, value } = await reader.read();
1737
+ if (done)
1738
+ break;
1739
+ chunks.push(value);
1740
+ }
1741
+ const totalLen = chunks.reduce((sum, c) => sum + c.length, 0);
1742
+ const result = new Uint8Array(totalLen);
1743
+ let offset = 0;
1744
+ for (const chunk of chunks) {
1745
+ result.set(chunk, offset);
1746
+ offset += chunk.length;
1747
+ }
1748
+ return result;
1749
+ }
1750
+ return parseInputData(data);
1751
+ }
1752
+ function printAlgorithmHelp() {
1753
+ console.log(`${BOLD}Usage:${RESET} dot hash <algorithm> <data> [options]
1754
+ `);
1755
+ console.log(`${BOLD}Algorithms:${RESET}`);
1756
+ for (const [name, algo] of Object.entries(ALGORITHMS)) {
1757
+ console.log(` ${CYAN}${name}${RESET} ${DIM}${algo.description} (${algo.outputLen} bytes)${RESET}`);
1758
+ }
1759
+ console.log(`
1760
+ ${BOLD}Options:${RESET}`);
1761
+ console.log(` ${CYAN}--file <path>${RESET} ${DIM}Hash file contents${RESET}`);
1762
+ console.log(` ${CYAN}--stdin${RESET} ${DIM}Read from stdin${RESET}`);
1763
+ console.log(` ${CYAN}--output json${RESET} ${DIM}Output as JSON${RESET}`);
1764
+ console.log(`
1765
+ ${BOLD}Examples:${RESET}`);
1766
+ console.log(` ${DIM}$ dot hash blake2b256 0xdeadbeef${RESET}`);
1767
+ console.log(` ${DIM}$ dot hash sha256 hello${RESET}`);
1768
+ console.log(` ${DIM}$ dot hash keccak256 --file ./data.bin${RESET}`);
1769
+ console.log(` ${DIM}$ echo -n "hello" | dot hash sha256 --stdin${RESET}`);
1770
+ }
1771
+ function registerHashCommand(cli) {
1772
+ cli.command("hash [algorithm] [data]", "Compute cryptographic hashes").option("--file <path>", "Hash file contents (raw bytes)").option("--stdin", "Read data from stdin").action(async (algorithm, data, opts) => {
1773
+ if (!algorithm) {
1774
+ printAlgorithmHelp();
1775
+ return;
1776
+ }
1777
+ if (!isValidAlgorithm(algorithm)) {
1778
+ throw new CliError(suggestMessage("algorithm", algorithm, getAlgorithmNames()));
1779
+ }
1780
+ const input = await resolveInput(data, opts);
1781
+ const hash = computeHash(algorithm, input);
1782
+ const hexHash = toHex(hash);
1783
+ const format = opts.output ?? "pretty";
1784
+ if (format === "json") {
1785
+ printResult({ algorithm, input: data ?? (opts.file ? `file:${opts.file}` : "stdin"), hash: hexHash }, "json");
1786
+ } else {
1787
+ console.log(hexHash);
1788
+ }
1789
+ });
1790
+ }
1492
1791
 
1493
1792
  // src/utils/errors.ts
1494
1793
  class CliError2 extends Error {
@@ -1497,6 +1796,8 @@ class CliError2 extends Error {
1497
1796
  this.name = "CliError";
1498
1797
  }
1499
1798
  }
1799
+ // package.json
1800
+ var version = "0.6.0";
1500
1801
 
1501
1802
  // src/cli.ts
1502
1803
  var cli = cac("dot");
@@ -1512,8 +1813,9 @@ registerQueryCommand(cli);
1512
1813
  registerConstCommand(cli);
1513
1814
  registerAccountCommands(cli);
1514
1815
  registerTxCommand(cli);
1816
+ registerHashCommand(cli);
1515
1817
  cli.help();
1516
- cli.version("0.3.0");
1818
+ cli.version(version);
1517
1819
  function handleError(err) {
1518
1820
  if (err instanceof CliError2) {
1519
1821
  console.error(`Error: ${err.message}`);
@@ -1524,11 +1826,15 @@ function handleError(err) {
1524
1826
  }
1525
1827
  process.exit(1);
1526
1828
  }
1527
- process.on("unhandledRejection", handleError);
1528
1829
  try {
1529
- cli.parse();
1830
+ cli.parse(process.argv, { run: false });
1530
1831
  if (!cli.matchedCommandName && !cli.options.help && !cli.options.version) {
1531
1832
  cli.outputHelp();
1833
+ } else {
1834
+ const result = cli.runMatchedCommand();
1835
+ if (result && typeof result.then === "function") {
1836
+ result.then(() => process.exit(0), handleError);
1837
+ }
1532
1838
  }
1533
1839
  } catch (err) {
1534
1840
  handleError(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polkadot-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool for querying Polkadot-ecosystem on-chain state",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "url": "git+https://github.com/peetzweg/polkadot-cli.git"
33
33
  },
34
34
  "dependencies": {
35
+ "@noble/hashes": "^2.0.1",
35
36
  "@polkadot-api/metadata-builders": "^0.13.9",
36
37
  "@polkadot-api/substrate-bindings": "^0.17.0",
37
38
  "@polkadot-api/view-builder": "^0.4.17",