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.
- package/README.md +116 -36
- package/dist/cli.mjs +357 -51
- 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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
98
|
-
dot
|
|
99
|
-
dot chain add westend --light-client
|
|
123
|
+
# Encode a remark call
|
|
124
|
+
dot tx System.remark 0xdeadbeef --encode
|
|
100
125
|
|
|
101
|
-
#
|
|
102
|
-
dot
|
|
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
|
-
#
|
|
105
|
-
dot
|
|
106
|
-
|
|
129
|
+
# Use encoded output with Sudo.sudo
|
|
130
|
+
dot tx Sudo.sudo $(dot tx System.remark 0xcafe --encode) --from alice
|
|
131
|
+
```
|
|
107
132
|
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
1387
|
+
result[name] = parseTypedArg(meta, entry, args[i]);
|
|
1324
1388
|
}
|
|
1325
1389
|
return result;
|
|
1326
1390
|
}
|
|
1327
|
-
function
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|