polkadot-cli 0.1.2 → 0.2.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 +41 -1
- package/dist/cli.mjs +597 -31
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# polkadot-cli
|
|
2
2
|
|
|
3
|
-
A command-line tool for
|
|
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.
|
|
4
4
|
|
|
5
5
|
Ships with Polkadot as the default chain. Add any Substrate-based chain by pointing to its RPC endpoint.
|
|
6
6
|
|
|
@@ -52,6 +52,45 @@ dot inspect System
|
|
|
52
52
|
dot inspect System.Account
|
|
53
53
|
```
|
|
54
54
|
|
|
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
|
+
### Submit extrinsics
|
|
77
|
+
|
|
78
|
+
Build, sign, and submit transactions. Arguments are parsed from metadata — the CLI knows the expected types for each call.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Simple remark
|
|
82
|
+
dot tx System.remark 0xdeadbeef --from alice
|
|
83
|
+
|
|
84
|
+
# Transfer (amount in plancks)
|
|
85
|
+
dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 1000000000000 --from alice
|
|
86
|
+
|
|
87
|
+
# Estimate fees without submitting
|
|
88
|
+
dot tx Balances.transferKeepAlive 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 1000000000000 --from alice --dry-run
|
|
89
|
+
|
|
90
|
+
# Batch multiple transfers with Utility.batchAll
|
|
91
|
+
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
|
+
```
|
|
93
|
+
|
|
55
94
|
### Manage chains
|
|
56
95
|
|
|
57
96
|
```bash
|
|
@@ -90,6 +129,7 @@ Config and metadata caches live in `~/.polkadot/`:
|
|
|
90
129
|
```
|
|
91
130
|
~/.polkadot/
|
|
92
131
|
├── config.json # chains and default chain
|
|
132
|
+
├── accounts.json # stored accounts (secrets encrypted at rest — coming soon)
|
|
93
133
|
└── chains/
|
|
94
134
|
└── polkadot/
|
|
95
135
|
└── metadata.bin # cached SCALE-encoded metadata
|
package/dist/cli.mjs
CHANGED
|
@@ -40,6 +40,9 @@ var DEFAULT_CONFIG = {
|
|
|
40
40
|
var DOT_DIR = join(homedir(), ".polkadot");
|
|
41
41
|
var CONFIG_PATH = join(DOT_DIR, "config.json");
|
|
42
42
|
var CHAINS_DIR = join(DOT_DIR, "chains");
|
|
43
|
+
function getConfigDir() {
|
|
44
|
+
return DOT_DIR;
|
|
45
|
+
}
|
|
43
46
|
function getChainDir(chainName) {
|
|
44
47
|
return join(CHAINS_DIR, chainName);
|
|
45
48
|
}
|
|
@@ -131,17 +134,30 @@ var KNOWN_CHAIN_SPECS = {
|
|
|
131
134
|
westend: "polkadot-api/chains/westend2",
|
|
132
135
|
paseo: "polkadot-api/chains/paseo"
|
|
133
136
|
};
|
|
137
|
+
function suppressWsNoise() {
|
|
138
|
+
const orig = console.error;
|
|
139
|
+
console.error = (...args) => {
|
|
140
|
+
if (typeof args[0] === "string" && args[0].includes("Unable to connect"))
|
|
141
|
+
return;
|
|
142
|
+
orig(...args);
|
|
143
|
+
};
|
|
144
|
+
return () => {
|
|
145
|
+
console.error = orig;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
134
148
|
async function createChainClient(chainName, chainConfig, rpcOverride) {
|
|
135
149
|
const useLight = !rpcOverride && chainConfig.lightClient;
|
|
150
|
+
const restoreConsole = suppressWsNoise();
|
|
136
151
|
let provider;
|
|
137
152
|
if (useLight) {
|
|
138
153
|
provider = await createSmoldotProvider(chainName);
|
|
139
154
|
} else {
|
|
140
155
|
const rpc = rpcOverride ?? chainConfig.rpc;
|
|
141
156
|
if (!rpc) {
|
|
157
|
+
restoreConsole();
|
|
142
158
|
throw new ConnectionError(`No RPC endpoint configured for chain "${chainName}". Use --rpc or configure one with: dot chain add ${chainName} --rpc <url>`);
|
|
143
159
|
}
|
|
144
|
-
provider = withPolkadotSdkCompat(getWsProvider(rpc));
|
|
160
|
+
provider = withPolkadotSdkCompat(getWsProvider(rpc, { timeout: 1e4 }));
|
|
145
161
|
}
|
|
146
162
|
const client = createClient(provider, {
|
|
147
163
|
getMetadata: async () => loadMetadata(chainName),
|
|
@@ -151,7 +167,10 @@ async function createChainClient(chainName, chainConfig, rpcOverride) {
|
|
|
151
167
|
});
|
|
152
168
|
return {
|
|
153
169
|
client,
|
|
154
|
-
destroy: () =>
|
|
170
|
+
destroy: () => {
|
|
171
|
+
client.destroy();
|
|
172
|
+
restoreConsole();
|
|
173
|
+
}
|
|
155
174
|
};
|
|
156
175
|
}
|
|
157
176
|
async function createSmoldotProvider(chainName) {
|
|
@@ -173,6 +192,7 @@ import {
|
|
|
173
192
|
unifyMetadata
|
|
174
193
|
} from "@polkadot-api/substrate-bindings";
|
|
175
194
|
import { getLookupFn, getDynamicBuilder } from "@polkadot-api/metadata-builders";
|
|
195
|
+
var METADATA_TIMEOUT_MS = 15000;
|
|
176
196
|
function parseMetadata(raw) {
|
|
177
197
|
const decoded = decAnyMetadata(raw);
|
|
178
198
|
const unified = unifyMetadata(decoded);
|
|
@@ -182,10 +202,19 @@ function parseMetadata(raw) {
|
|
|
182
202
|
}
|
|
183
203
|
async function fetchMetadataFromChain(clientHandle, chainName) {
|
|
184
204
|
const { client } = clientHandle;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
try {
|
|
206
|
+
const hex = await Promise.race([
|
|
207
|
+
client._request("state_getMetadata", []),
|
|
208
|
+
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))
|
|
209
|
+
]);
|
|
210
|
+
const bytes = hexToBytes(hex);
|
|
211
|
+
await saveMetadata(chainName, bytes);
|
|
212
|
+
return bytes;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (err instanceof ConnectionError)
|
|
215
|
+
throw err;
|
|
216
|
+
throw new ConnectionError(`Failed to fetch metadata for "${chainName}": ${err instanceof Error ? err.message : err}. ` + "Check that the RPC endpoint is correct and reachable.");
|
|
217
|
+
}
|
|
189
218
|
}
|
|
190
219
|
async function getOrFetchMetadata(chainName, clientHandle) {
|
|
191
220
|
let raw = await loadMetadata(chainName);
|
|
@@ -213,9 +242,35 @@ function listPallets(meta) {
|
|
|
213
242
|
name: c.name,
|
|
214
243
|
docs: c.docs ?? [],
|
|
215
244
|
typeId: c.type
|
|
216
|
-
}))
|
|
245
|
+
})),
|
|
246
|
+
calls: extractCalls(meta, p.calls)
|
|
217
247
|
}));
|
|
218
248
|
}
|
|
249
|
+
function extractCalls(meta, callsRef) {
|
|
250
|
+
if (!callsRef)
|
|
251
|
+
return [];
|
|
252
|
+
try {
|
|
253
|
+
const entry = meta.lookup(callsRef.type);
|
|
254
|
+
if (entry.type !== "enum")
|
|
255
|
+
return [];
|
|
256
|
+
return Object.entries(entry.value).map(([name, variant]) => ({
|
|
257
|
+
name,
|
|
258
|
+
docs: variant.docs ?? [],
|
|
259
|
+
typeId: resolveCallTypeId(variant)
|
|
260
|
+
}));
|
|
261
|
+
} catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function resolveCallTypeId(variant) {
|
|
266
|
+
if (variant.type === "lookupEntry")
|
|
267
|
+
return variant.value?.id ?? null;
|
|
268
|
+
if (variant.type === "struct")
|
|
269
|
+
return null;
|
|
270
|
+
if (variant.type === "void" || variant.type === "empty")
|
|
271
|
+
return null;
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
219
274
|
function findPallet(meta, palletName) {
|
|
220
275
|
const pallets = listPallets(meta);
|
|
221
276
|
return pallets.find((p) => p.name.toLowerCase() === palletName.toLowerCase());
|
|
@@ -386,17 +441,18 @@ async function chainAdd(name, opts) {
|
|
|
386
441
|
console.error(" dot chain add <name> --light-client");
|
|
387
442
|
process.exit(1);
|
|
388
443
|
}
|
|
389
|
-
const
|
|
390
|
-
config.chains[name] = {
|
|
444
|
+
const chainConfig = {
|
|
391
445
|
rpc: opts.rpc ?? "",
|
|
392
446
|
...opts.lightClient ? { lightClient: true } : {}
|
|
393
447
|
};
|
|
394
|
-
await saveConfig(config);
|
|
395
448
|
console.log(`Connecting to ${name}...`);
|
|
396
|
-
const clientHandle = await createChainClient(name,
|
|
449
|
+
const clientHandle = await createChainClient(name, chainConfig, opts.rpc);
|
|
397
450
|
try {
|
|
398
451
|
console.log("Fetching metadata...");
|
|
399
452
|
await fetchMetadataFromChain(clientHandle, name);
|
|
453
|
+
const config = await loadConfig();
|
|
454
|
+
config.chains[name] = chainConfig;
|
|
455
|
+
await saveConfig(config);
|
|
400
456
|
console.log(`Chain "${name}" added successfully.`);
|
|
401
457
|
} finally {
|
|
402
458
|
clientHandle.destroy();
|
|
@@ -607,6 +663,26 @@ function registerInspectCommand(cli) {
|
|
|
607
663
|
});
|
|
608
664
|
}
|
|
609
665
|
|
|
666
|
+
// src/utils/parse-value.ts
|
|
667
|
+
function parseValue(arg) {
|
|
668
|
+
if (/^\d+$/.test(arg))
|
|
669
|
+
return parseInt(arg, 10);
|
|
670
|
+
if (/^\d{16,}$/.test(arg))
|
|
671
|
+
return BigInt(arg);
|
|
672
|
+
if (/^0x[0-9a-fA-F]+$/.test(arg))
|
|
673
|
+
return arg;
|
|
674
|
+
if (arg === "true")
|
|
675
|
+
return true;
|
|
676
|
+
if (arg === "false")
|
|
677
|
+
return false;
|
|
678
|
+
if (arg.startsWith("{") || arg.startsWith("[")) {
|
|
679
|
+
try {
|
|
680
|
+
return JSON.parse(arg);
|
|
681
|
+
} catch {}
|
|
682
|
+
}
|
|
683
|
+
return arg;
|
|
684
|
+
}
|
|
685
|
+
|
|
610
686
|
// src/commands/query.ts
|
|
611
687
|
var DEFAULT_LIMIT = 100;
|
|
612
688
|
function registerQueryCommand(cli) {
|
|
@@ -643,7 +719,7 @@ function registerQueryCommand(cli) {
|
|
|
643
719
|
}
|
|
644
720
|
const unsafeApi = clientHandle.client.getUnsafeApi();
|
|
645
721
|
const storageApi = unsafeApi.query[palletInfo.name][storageItem.name];
|
|
646
|
-
const parsedKeys = keys.map(
|
|
722
|
+
const parsedKeys = keys.map(parseValue);
|
|
647
723
|
const format = opts.output ?? "pretty";
|
|
648
724
|
if (storageItem.type === "map" && parsedKeys.length === 0) {
|
|
649
725
|
const entries = await storageApi.getEntries();
|
|
@@ -667,24 +743,6 @@ ${DIM}Showing ${limit} of ${entries.length} entries. Use --limit 0 for all.${RES
|
|
|
667
743
|
}
|
|
668
744
|
});
|
|
669
745
|
}
|
|
670
|
-
function parseKeyArg(arg) {
|
|
671
|
-
if (/^\d+$/.test(arg))
|
|
672
|
-
return parseInt(arg, 10);
|
|
673
|
-
if (/^\d{16,}$/.test(arg))
|
|
674
|
-
return BigInt(arg);
|
|
675
|
-
if (/^0x[0-9a-fA-F]+$/.test(arg))
|
|
676
|
-
return arg;
|
|
677
|
-
if (arg === "true")
|
|
678
|
-
return true;
|
|
679
|
-
if (arg === "false")
|
|
680
|
-
return false;
|
|
681
|
-
if (arg.startsWith("{") || arg.startsWith("[")) {
|
|
682
|
-
try {
|
|
683
|
-
return JSON.parse(arg);
|
|
684
|
-
} catch {}
|
|
685
|
-
}
|
|
686
|
-
return arg;
|
|
687
|
-
}
|
|
688
746
|
|
|
689
747
|
// src/commands/const.ts
|
|
690
748
|
function registerConstCommand(cli) {
|
|
@@ -723,6 +781,512 @@ function registerConstCommand(cli) {
|
|
|
723
781
|
});
|
|
724
782
|
}
|
|
725
783
|
|
|
784
|
+
// src/config/accounts-store.ts
|
|
785
|
+
import { join as join2 } from "node:path";
|
|
786
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, access as access2 } from "node:fs/promises";
|
|
787
|
+
var ACCOUNTS_PATH = join2(getConfigDir(), "accounts.json");
|
|
788
|
+
async function ensureDir2(dir) {
|
|
789
|
+
await mkdir2(dir, { recursive: true });
|
|
790
|
+
}
|
|
791
|
+
async function fileExists2(path) {
|
|
792
|
+
try {
|
|
793
|
+
await access2(path);
|
|
794
|
+
return true;
|
|
795
|
+
} catch {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function loadAccounts() {
|
|
800
|
+
await ensureDir2(getConfigDir());
|
|
801
|
+
if (await fileExists2(ACCOUNTS_PATH)) {
|
|
802
|
+
const data = await readFile2(ACCOUNTS_PATH, "utf-8");
|
|
803
|
+
return JSON.parse(data);
|
|
804
|
+
}
|
|
805
|
+
return { accounts: [] };
|
|
806
|
+
}
|
|
807
|
+
async function saveAccounts(file) {
|
|
808
|
+
await ensureDir2(getConfigDir());
|
|
809
|
+
await writeFile2(ACCOUNTS_PATH, JSON.stringify(file, null, 2) + `
|
|
810
|
+
`);
|
|
811
|
+
}
|
|
812
|
+
function findAccount(file, name) {
|
|
813
|
+
return file.accounts.find((a) => a.name.toLowerCase() === name.toLowerCase());
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/core/accounts.ts
|
|
817
|
+
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
|
|
818
|
+
import {
|
|
819
|
+
DEV_PHRASE,
|
|
820
|
+
mnemonicToEntropy,
|
|
821
|
+
entropyToMiniSecret,
|
|
822
|
+
generateMnemonic,
|
|
823
|
+
validateMnemonic,
|
|
824
|
+
ss58Address
|
|
825
|
+
} from "@polkadot-labs/hdkd-helpers";
|
|
826
|
+
import { getPolkadotSigner } from "polkadot-api/signer";
|
|
827
|
+
var DEV_NAMES = [
|
|
828
|
+
"alice",
|
|
829
|
+
"bob",
|
|
830
|
+
"charlie",
|
|
831
|
+
"dave",
|
|
832
|
+
"eve",
|
|
833
|
+
"ferdie"
|
|
834
|
+
];
|
|
835
|
+
function isDevAccount(name) {
|
|
836
|
+
return DEV_NAMES.includes(name.toLowerCase());
|
|
837
|
+
}
|
|
838
|
+
function devDerivationPath(name) {
|
|
839
|
+
return "//" + name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
|
|
840
|
+
}
|
|
841
|
+
function deriveFromMnemonic(mnemonic, path) {
|
|
842
|
+
const entropy = mnemonicToEntropy(mnemonic);
|
|
843
|
+
const miniSecret = entropyToMiniSecret(entropy);
|
|
844
|
+
const derive = sr25519CreateDerive(miniSecret);
|
|
845
|
+
return derive(path);
|
|
846
|
+
}
|
|
847
|
+
function deriveFromHexSeed(hexSeed, path) {
|
|
848
|
+
const clean = hexSeed.startsWith("0x") ? hexSeed.slice(2) : hexSeed;
|
|
849
|
+
const seed = new Uint8Array(clean.length / 2);
|
|
850
|
+
for (let i = 0;i < clean.length; i += 2) {
|
|
851
|
+
seed[i / 2] = parseInt(clean.substring(i, i + 2), 16);
|
|
852
|
+
}
|
|
853
|
+
const derive = sr25519CreateDerive(seed);
|
|
854
|
+
return derive(path);
|
|
855
|
+
}
|
|
856
|
+
function getDevKeypair(name) {
|
|
857
|
+
const path = devDerivationPath(name);
|
|
858
|
+
return deriveFromMnemonic(DEV_PHRASE, path);
|
|
859
|
+
}
|
|
860
|
+
function getDevAddress(name, prefix = 42) {
|
|
861
|
+
const keypair = getDevKeypair(name);
|
|
862
|
+
return ss58Address(keypair.publicKey, prefix);
|
|
863
|
+
}
|
|
864
|
+
function createNewAccount() {
|
|
865
|
+
const mnemonic = generateMnemonic();
|
|
866
|
+
const entropy = mnemonicToEntropy(mnemonic);
|
|
867
|
+
const miniSecret = entropyToMiniSecret(entropy);
|
|
868
|
+
const derive = sr25519CreateDerive(miniSecret);
|
|
869
|
+
const keypair = derive("");
|
|
870
|
+
return { mnemonic, publicKey: keypair.publicKey };
|
|
871
|
+
}
|
|
872
|
+
function importAccount(secret) {
|
|
873
|
+
const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(secret);
|
|
874
|
+
if (isHexSeed) {
|
|
875
|
+
const keypair2 = deriveFromHexSeed(secret, "");
|
|
876
|
+
return { publicKey: keypair2.publicKey };
|
|
877
|
+
}
|
|
878
|
+
if (!validateMnemonic(secret)) {
|
|
879
|
+
throw new Error("Invalid secret. Expected a 0x-prefixed 32-byte hex seed or a valid BIP39 mnemonic.");
|
|
880
|
+
}
|
|
881
|
+
const keypair = deriveFromMnemonic(secret, "");
|
|
882
|
+
return { publicKey: keypair.publicKey };
|
|
883
|
+
}
|
|
884
|
+
function publicKeyToHex(publicKey) {
|
|
885
|
+
return "0x" + Array.from(publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
886
|
+
}
|
|
887
|
+
function toSs58(publicKey, prefix = 42) {
|
|
888
|
+
if (typeof publicKey === "string") {
|
|
889
|
+
const clean = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
|
|
890
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
891
|
+
for (let i = 0;i < clean.length; i += 2) {
|
|
892
|
+
bytes[i / 2] = parseInt(clean.substring(i, i + 2), 16);
|
|
893
|
+
}
|
|
894
|
+
return ss58Address(bytes, prefix);
|
|
895
|
+
}
|
|
896
|
+
return ss58Address(publicKey, prefix);
|
|
897
|
+
}
|
|
898
|
+
async function resolveAccountSigner(name) {
|
|
899
|
+
if (isDevAccount(name)) {
|
|
900
|
+
const keypair2 = getDevKeypair(name);
|
|
901
|
+
return getPolkadotSigner(keypair2.publicKey, "Sr25519", keypair2.sign);
|
|
902
|
+
}
|
|
903
|
+
const accountsFile = await loadAccounts();
|
|
904
|
+
const account = findAccount(accountsFile, name);
|
|
905
|
+
if (!account) {
|
|
906
|
+
const available = [
|
|
907
|
+
...DEV_NAMES,
|
|
908
|
+
...accountsFile.accounts.map((a) => a.name)
|
|
909
|
+
];
|
|
910
|
+
throw new Error(`Unknown account "${name}". Available accounts: ${available.join(", ")}`);
|
|
911
|
+
}
|
|
912
|
+
const isHexSeed = /^0x[0-9a-fA-F]{64}$/.test(account.secret);
|
|
913
|
+
const keypair = isHexSeed ? deriveFromHexSeed(account.secret, account.derivationPath) : deriveFromMnemonic(account.secret, account.derivationPath);
|
|
914
|
+
return getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/commands/account.ts
|
|
918
|
+
var ACCOUNT_HELP = `
|
|
919
|
+
${BOLD}Usage:${RESET}
|
|
920
|
+
$ dot account create <name> Create a new account
|
|
921
|
+
$ dot account import <name> --secret <s> Import from mnemonic or hex seed
|
|
922
|
+
$ dot account list List all accounts
|
|
923
|
+
$ dot account remove <name> Remove a stored account
|
|
924
|
+
|
|
925
|
+
${BOLD}Examples:${RESET}
|
|
926
|
+
$ dot account create my-validator
|
|
927
|
+
$ dot account import treasury --secret "word1 word2 ... word12"
|
|
928
|
+
$ dot account import raw-key --secret 0xabcdef...
|
|
929
|
+
$ dot account list
|
|
930
|
+
$ dot account remove my-validator
|
|
931
|
+
`.trimStart();
|
|
932
|
+
function registerAccountCommands(cli) {
|
|
933
|
+
cli.command("account [action] [name]", "Manage local accounts (create, import, list, remove)").option("--secret <value>", "Secret key (mnemonic or hex seed) for import").action(async (action, name, opts) => {
|
|
934
|
+
if (!action) {
|
|
935
|
+
console.log(ACCOUNT_HELP);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
switch (action) {
|
|
939
|
+
case "create":
|
|
940
|
+
return accountCreate(name);
|
|
941
|
+
case "import":
|
|
942
|
+
return accountImport(name, opts);
|
|
943
|
+
case "list":
|
|
944
|
+
return accountList();
|
|
945
|
+
case "remove":
|
|
946
|
+
return accountRemove(name);
|
|
947
|
+
default:
|
|
948
|
+
console.error(`Unknown action "${action}".
|
|
949
|
+
`);
|
|
950
|
+
console.log(ACCOUNT_HELP);
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async function accountCreate(name) {
|
|
956
|
+
if (!name) {
|
|
957
|
+
console.error(`Account name is required.
|
|
958
|
+
`);
|
|
959
|
+
console.error("Usage: dot account create <name>");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
if (isDevAccount(name)) {
|
|
963
|
+
throw new Error(`"${name}" is a built-in dev account and cannot be used as a custom account name.`);
|
|
964
|
+
}
|
|
965
|
+
const accountsFile = await loadAccounts();
|
|
966
|
+
if (findAccount(accountsFile, name)) {
|
|
967
|
+
throw new Error(`Account "${name}" already exists.`);
|
|
968
|
+
}
|
|
969
|
+
const { mnemonic, publicKey } = createNewAccount();
|
|
970
|
+
const hexPub = publicKeyToHex(publicKey);
|
|
971
|
+
const address = toSs58(publicKey);
|
|
972
|
+
accountsFile.accounts.push({
|
|
973
|
+
name,
|
|
974
|
+
secret: mnemonic,
|
|
975
|
+
publicKey: hexPub,
|
|
976
|
+
derivationPath: ""
|
|
977
|
+
});
|
|
978
|
+
await saveAccounts(accountsFile);
|
|
979
|
+
printHeading("Account Created");
|
|
980
|
+
console.log(` ${BOLD}Name:${RESET} ${name}`);
|
|
981
|
+
console.log(` ${BOLD}Address:${RESET} ${address}`);
|
|
982
|
+
console.log(` ${BOLD}Mnemonic:${RESET} ${mnemonic}`);
|
|
983
|
+
console.log();
|
|
984
|
+
console.log(` ${YELLOW}Save this mnemonic phrase! It is the only way to recover this account.${RESET}`);
|
|
985
|
+
console.log();
|
|
986
|
+
}
|
|
987
|
+
async function accountImport(name, opts) {
|
|
988
|
+
if (!name) {
|
|
989
|
+
console.error(`Account name is required.
|
|
990
|
+
`);
|
|
991
|
+
console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
if (!opts.secret) {
|
|
995
|
+
console.error(`--secret is required.
|
|
996
|
+
`);
|
|
997
|
+
console.error('Usage: dot account import <name> --secret "mnemonic or hex seed"');
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
if (isDevAccount(name)) {
|
|
1001
|
+
throw new Error(`"${name}" is a built-in dev account and cannot be used as a custom account name.`);
|
|
1002
|
+
}
|
|
1003
|
+
const accountsFile = await loadAccounts();
|
|
1004
|
+
if (findAccount(accountsFile, name)) {
|
|
1005
|
+
throw new Error(`Account "${name}" already exists.`);
|
|
1006
|
+
}
|
|
1007
|
+
const { publicKey } = importAccount(opts.secret);
|
|
1008
|
+
const hexPub = publicKeyToHex(publicKey);
|
|
1009
|
+
const address = toSs58(publicKey);
|
|
1010
|
+
accountsFile.accounts.push({
|
|
1011
|
+
name,
|
|
1012
|
+
secret: opts.secret,
|
|
1013
|
+
publicKey: hexPub,
|
|
1014
|
+
derivationPath: ""
|
|
1015
|
+
});
|
|
1016
|
+
await saveAccounts(accountsFile);
|
|
1017
|
+
printHeading("Account Imported");
|
|
1018
|
+
console.log(` ${BOLD}Name:${RESET} ${name}`);
|
|
1019
|
+
console.log(` ${BOLD}Address:${RESET} ${address}`);
|
|
1020
|
+
console.log();
|
|
1021
|
+
}
|
|
1022
|
+
async function accountList() {
|
|
1023
|
+
printHeading("Dev Accounts");
|
|
1024
|
+
for (const name of DEV_NAMES) {
|
|
1025
|
+
const display = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1026
|
+
const address = getDevAddress(name);
|
|
1027
|
+
printItem(display, address);
|
|
1028
|
+
}
|
|
1029
|
+
const accountsFile = await loadAccounts();
|
|
1030
|
+
if (accountsFile.accounts.length > 0) {
|
|
1031
|
+
printHeading("Stored Accounts");
|
|
1032
|
+
for (const account of accountsFile.accounts) {
|
|
1033
|
+
const address = toSs58(account.publicKey);
|
|
1034
|
+
printItem(account.name, address);
|
|
1035
|
+
}
|
|
1036
|
+
} else {
|
|
1037
|
+
printHeading("Stored Accounts");
|
|
1038
|
+
console.log(" (none)");
|
|
1039
|
+
}
|
|
1040
|
+
console.log();
|
|
1041
|
+
}
|
|
1042
|
+
async function accountRemove(name) {
|
|
1043
|
+
if (!name) {
|
|
1044
|
+
console.error(`Account name is required.
|
|
1045
|
+
`);
|
|
1046
|
+
console.error("Usage: dot account remove <name>");
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
if (isDevAccount(name)) {
|
|
1050
|
+
throw new Error("Cannot remove built-in dev accounts.");
|
|
1051
|
+
}
|
|
1052
|
+
const accountsFile = await loadAccounts();
|
|
1053
|
+
const idx = accountsFile.accounts.findIndex((a) => a.name.toLowerCase() === name.toLowerCase());
|
|
1054
|
+
if (idx === -1) {
|
|
1055
|
+
throw new Error(`Account "${name}" not found.`);
|
|
1056
|
+
}
|
|
1057
|
+
accountsFile.accounts.splice(idx, 1);
|
|
1058
|
+
await saveAccounts(accountsFile);
|
|
1059
|
+
console.log(`Account "${name}" removed.`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/commands/tx.ts
|
|
1063
|
+
function registerTxCommand(cli) {
|
|
1064
|
+
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").action(async (target, args, opts) => {
|
|
1065
|
+
if (!target || !opts.from) {
|
|
1066
|
+
console.log("Usage: dot tx <Pallet.Call> [...args] --from <account> [--dry-run]");
|
|
1067
|
+
console.log("");
|
|
1068
|
+
console.log("Examples:");
|
|
1069
|
+
console.log(" $ dot tx Balances.transferKeepAlive 5FHn... 1000000000000 --from alice");
|
|
1070
|
+
console.log(" $ dot tx System.remark 0xdeadbeef --from alice --dry-run");
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const config = await loadConfig();
|
|
1074
|
+
const { name: chainName, chain: chainConfig } = resolveChain(config, opts.chain);
|
|
1075
|
+
const { pallet, item: callName } = parseTarget(target);
|
|
1076
|
+
const signer = await resolveAccountSigner(opts.from);
|
|
1077
|
+
const clientHandle = await createChainClient(chainName, chainConfig, opts.rpc);
|
|
1078
|
+
try {
|
|
1079
|
+
const meta = await getOrFetchMetadata(chainName, clientHandle);
|
|
1080
|
+
const palletNames = getPalletNames(meta);
|
|
1081
|
+
const palletInfo = findPallet(meta, pallet);
|
|
1082
|
+
if (!palletInfo) {
|
|
1083
|
+
throw new Error(suggestMessage("pallet", pallet, palletNames));
|
|
1084
|
+
}
|
|
1085
|
+
const callInfo = palletInfo.calls.find((c) => c.name.toLowerCase() === callName.toLowerCase());
|
|
1086
|
+
if (!callInfo) {
|
|
1087
|
+
const callNames = palletInfo.calls.map((c) => c.name);
|
|
1088
|
+
throw new Error(suggestMessage(`call in ${palletInfo.name}`, callName, callNames));
|
|
1089
|
+
}
|
|
1090
|
+
const callData = parseCallArgs(meta, palletInfo.name, callInfo.name, args);
|
|
1091
|
+
const unsafeApi = clientHandle.client.getUnsafeApi();
|
|
1092
|
+
const tx = unsafeApi.tx[palletInfo.name][callInfo.name](callData);
|
|
1093
|
+
if (opts.dryRun) {
|
|
1094
|
+
const signerAddress = toSs58(signer.publicKey);
|
|
1095
|
+
console.log(` ${BOLD}From:${RESET} ${opts.from} (${signerAddress})`);
|
|
1096
|
+
try {
|
|
1097
|
+
const fees = await tx.getEstimatedFees(signer.publicKey);
|
|
1098
|
+
console.log(` ${BOLD}Estimated fees:${RESET} ${fees}`);
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
console.log(` ${BOLD}Estimated fees:${RESET} ${YELLOW}unable to estimate${RESET}`);
|
|
1101
|
+
console.log(` ${DIM}${err.message ?? err}${RESET}`);
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
console.log("Signing and submitting...");
|
|
1106
|
+
const result = await tx.signAndSubmit(signer);
|
|
1107
|
+
console.log();
|
|
1108
|
+
console.log(` ${BOLD}Tx:${RESET} ${result.txHash}`);
|
|
1109
|
+
if (result.block) {
|
|
1110
|
+
console.log(` ${BOLD}Block:${RESET} #${result.block.number} (${result.block.hash})`);
|
|
1111
|
+
}
|
|
1112
|
+
if (result.dispatchError) {
|
|
1113
|
+
console.log(` ${BOLD}Status:${RESET} ${YELLOW}dispatch error${RESET}`);
|
|
1114
|
+
console.log(` ${BOLD}Error:${RESET} ${result.dispatchError.type}${result.dispatchError.value ? ": " + JSON.stringify(result.dispatchError.value) : ""}`);
|
|
1115
|
+
} else {
|
|
1116
|
+
console.log(` ${BOLD}Status:${RESET} ${GREEN}ok${RESET}`);
|
|
1117
|
+
}
|
|
1118
|
+
if (result.events && result.events.length > 0) {
|
|
1119
|
+
console.log(` ${BOLD}Events:${RESET}`);
|
|
1120
|
+
for (const event of result.events) {
|
|
1121
|
+
const name = `${CYAN}${event.type}${RESET}.${CYAN}${event.value?.type ?? ""}${RESET}`;
|
|
1122
|
+
const payload = event.value?.value;
|
|
1123
|
+
if (payload && typeof payload === "object") {
|
|
1124
|
+
const fields = Object.entries(payload).map(([k, v]) => `${k}: ${formatEventValue(v)}`).join(", ");
|
|
1125
|
+
console.log(` ${name} { ${fields} }`);
|
|
1126
|
+
} else {
|
|
1127
|
+
console.log(` ${name}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
console.log();
|
|
1132
|
+
} finally {
|
|
1133
|
+
clientHandle.destroy();
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
function formatEventValue(v) {
|
|
1138
|
+
if (typeof v === "bigint")
|
|
1139
|
+
return v.toString();
|
|
1140
|
+
if (typeof v === "string")
|
|
1141
|
+
return v;
|
|
1142
|
+
if (typeof v === "number")
|
|
1143
|
+
return v.toString();
|
|
1144
|
+
if (typeof v === "boolean")
|
|
1145
|
+
return v.toString();
|
|
1146
|
+
if (v === null || v === undefined)
|
|
1147
|
+
return "null";
|
|
1148
|
+
return JSON.stringify(v, (_k, val) => typeof val === "bigint" ? val.toString() : val);
|
|
1149
|
+
}
|
|
1150
|
+
function parseCallArgs(meta, palletName, callName, args) {
|
|
1151
|
+
const palletMeta = meta.unified.pallets.find((p) => p.name === palletName);
|
|
1152
|
+
if (!palletMeta?.calls)
|
|
1153
|
+
return;
|
|
1154
|
+
const callsEntry = meta.lookup(palletMeta.calls.type);
|
|
1155
|
+
if (callsEntry.type !== "enum")
|
|
1156
|
+
return;
|
|
1157
|
+
const variant = callsEntry.value[callName];
|
|
1158
|
+
if (!variant)
|
|
1159
|
+
return;
|
|
1160
|
+
if (variant.type === "void") {
|
|
1161
|
+
if (args.length > 0) {
|
|
1162
|
+
throw new Error(`${palletName}.${callName} takes no arguments, but ${args.length} provided.`);
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (variant.type === "struct") {
|
|
1167
|
+
return parseStructArgs(meta.lookup, variant.value, args, `${palletName}.${callName}`);
|
|
1168
|
+
}
|
|
1169
|
+
if (variant.type === "lookupEntry") {
|
|
1170
|
+
const inner = variant.value;
|
|
1171
|
+
if (inner.type === "struct") {
|
|
1172
|
+
return parseStructArgs(meta.lookup, inner.value, args, `${palletName}.${callName}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (inner.type === "void")
|
|
1175
|
+
return;
|
|
1176
|
+
if (args.length !== 1) {
|
|
1177
|
+
throw new Error(`${palletName}.${callName} takes 1 argument (${describeType(meta.lookup, inner.id)}), but ${args.length} provided.`);
|
|
1178
|
+
}
|
|
1179
|
+
return parseTypedArg(meta.lookup, inner, args[0]);
|
|
1180
|
+
}
|
|
1181
|
+
if (variant.type === "tuple") {
|
|
1182
|
+
const entries = variant.value;
|
|
1183
|
+
if (args.length !== entries.length) {
|
|
1184
|
+
throw new Error(`${palletName}.${callName} takes ${entries.length} arguments, but ${args.length} provided.`);
|
|
1185
|
+
}
|
|
1186
|
+
return entries.map((entry, i) => parseTypedArg(meta.lookup, entry, args[i]));
|
|
1187
|
+
}
|
|
1188
|
+
return args.length === 0 ? undefined : args.map(parseValue);
|
|
1189
|
+
}
|
|
1190
|
+
function parseStructArgs(lookup, fields, args, callLabel) {
|
|
1191
|
+
const fieldNames = Object.keys(fields);
|
|
1192
|
+
if (args.length !== fieldNames.length) {
|
|
1193
|
+
const expected = fieldNames.map((name) => `${name}: ${describeType(lookup, fields[name].id)}`).join(", ");
|
|
1194
|
+
throw new Error(`${callLabel} takes ${fieldNames.length} argument(s): ${expected}
|
|
1195
|
+
` + ` Got ${args.length} argument(s).`);
|
|
1196
|
+
}
|
|
1197
|
+
const result = {};
|
|
1198
|
+
for (let i = 0;i < fieldNames.length; i++) {
|
|
1199
|
+
const name = fieldNames[i];
|
|
1200
|
+
const entry = fields[name];
|
|
1201
|
+
result[name] = parseTypedArg(lookup, entry, args[i]);
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
}
|
|
1205
|
+
function parseTypedArg(lookup, entry, arg) {
|
|
1206
|
+
switch (entry.type) {
|
|
1207
|
+
case "primitive":
|
|
1208
|
+
return parsePrimitive(entry.value, arg);
|
|
1209
|
+
case "compact":
|
|
1210
|
+
return entry.isBig ? BigInt(arg) : parseInt(arg, 10);
|
|
1211
|
+
case "AccountId32":
|
|
1212
|
+
case "AccountId20":
|
|
1213
|
+
return arg;
|
|
1214
|
+
case "option": {
|
|
1215
|
+
if (arg === "null" || arg === "undefined" || arg === "none") {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
return parseTypedArg(lookup, entry.value, arg);
|
|
1219
|
+
}
|
|
1220
|
+
case "enum": {
|
|
1221
|
+
if (arg.startsWith("{")) {
|
|
1222
|
+
try {
|
|
1223
|
+
return JSON.parse(arg);
|
|
1224
|
+
} catch {}
|
|
1225
|
+
}
|
|
1226
|
+
const variants = Object.keys(entry.value);
|
|
1227
|
+
const matched = variants.find((v) => v.toLowerCase() === arg.toLowerCase());
|
|
1228
|
+
if (matched) {
|
|
1229
|
+
const variant = entry.value[matched];
|
|
1230
|
+
if (variant.type === "void") {
|
|
1231
|
+
return { type: matched };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return parseValue(arg);
|
|
1235
|
+
}
|
|
1236
|
+
case "sequence":
|
|
1237
|
+
case "array":
|
|
1238
|
+
if (arg.startsWith("[")) {
|
|
1239
|
+
try {
|
|
1240
|
+
return JSON.parse(arg);
|
|
1241
|
+
} catch {}
|
|
1242
|
+
}
|
|
1243
|
+
if (/^0x[0-9a-fA-F]*$/.test(arg))
|
|
1244
|
+
return arg;
|
|
1245
|
+
return parseValue(arg);
|
|
1246
|
+
case "struct":
|
|
1247
|
+
if (arg.startsWith("{")) {
|
|
1248
|
+
try {
|
|
1249
|
+
return JSON.parse(arg);
|
|
1250
|
+
} catch {}
|
|
1251
|
+
}
|
|
1252
|
+
return parseValue(arg);
|
|
1253
|
+
case "tuple":
|
|
1254
|
+
if (arg.startsWith("[")) {
|
|
1255
|
+
try {
|
|
1256
|
+
return JSON.parse(arg);
|
|
1257
|
+
} catch {}
|
|
1258
|
+
}
|
|
1259
|
+
return parseValue(arg);
|
|
1260
|
+
default:
|
|
1261
|
+
return parseValue(arg);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function parsePrimitive(prim, arg) {
|
|
1265
|
+
switch (prim) {
|
|
1266
|
+
case "bool":
|
|
1267
|
+
return arg === "true";
|
|
1268
|
+
case "char":
|
|
1269
|
+
case "str":
|
|
1270
|
+
return arg;
|
|
1271
|
+
case "u8":
|
|
1272
|
+
case "u16":
|
|
1273
|
+
case "u32":
|
|
1274
|
+
case "i8":
|
|
1275
|
+
case "i16":
|
|
1276
|
+
case "i32":
|
|
1277
|
+
return parseInt(arg, 10);
|
|
1278
|
+
case "u64":
|
|
1279
|
+
case "u128":
|
|
1280
|
+
case "u256":
|
|
1281
|
+
case "i64":
|
|
1282
|
+
case "i128":
|
|
1283
|
+
case "i256":
|
|
1284
|
+
return BigInt(arg);
|
|
1285
|
+
default:
|
|
1286
|
+
return parseValue(arg);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
726
1290
|
// src/utils/errors.ts
|
|
727
1291
|
class CliError2 extends Error {
|
|
728
1292
|
constructor(message) {
|
|
@@ -743,8 +1307,10 @@ registerChainCommands(cli);
|
|
|
743
1307
|
registerInspectCommand(cli);
|
|
744
1308
|
registerQueryCommand(cli);
|
|
745
1309
|
registerConstCommand(cli);
|
|
1310
|
+
registerAccountCommands(cli);
|
|
1311
|
+
registerTxCommand(cli);
|
|
746
1312
|
cli.help();
|
|
747
|
-
cli.version("0.
|
|
1313
|
+
cli.version("0.2.0");
|
|
748
1314
|
function handleError(err) {
|
|
749
1315
|
if (err instanceof CliError2) {
|
|
750
1316
|
console.error(`Error: ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polkadot-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI tool for querying Polkadot-ecosystem on-chain state",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
"url": "git+https://github.com/peetzweg/polkadot-cli.git"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"cac": "^6.7.14",
|
|
36
|
-
"polkadot-api": "^1.23.3",
|
|
37
35
|
"@polkadot-api/metadata-builders": "^0.13.9",
|
|
38
36
|
"@polkadot-api/substrate-bindings": "^0.17.0",
|
|
39
|
-
"@polkadot-api/view-builder": "^0.4.17"
|
|
37
|
+
"@polkadot-api/view-builder": "^0.4.17",
|
|
38
|
+
"@polkadot-labs/hdkd": "^0.0.26",
|
|
39
|
+
"@polkadot-labs/hdkd-helpers": "^0.0.27",
|
|
40
|
+
"cac": "^6.7.14",
|
|
41
|
+
"polkadot-api": "^1.23.3"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@changesets/cli": "^2.29.4",
|