humanfx-wallet-cli 0.1.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/dist/args.js ADDED
@@ -0,0 +1,27 @@
1
+ /** Minimal `--flag value` / `--flag` (boolean) parser — no external dep needed for this CLI's
2
+ * small, fixed set of flags. */
3
+ export function parseFlags(args) {
4
+ const flags = {};
5
+ for (let i = 0; i < args.length; i += 1) {
6
+ const arg = args[i];
7
+ if (!arg.startsWith('--'))
8
+ continue;
9
+ const name = arg.slice(2);
10
+ const next = args[i + 1];
11
+ if (next !== undefined && !next.startsWith('--')) {
12
+ flags[name] = next;
13
+ i += 1;
14
+ }
15
+ else {
16
+ flags[name] = true;
17
+ }
18
+ }
19
+ return flags;
20
+ }
21
+ export function requireFlag(flags, name) {
22
+ const value = flags[name];
23
+ if (typeof value !== 'string' || !value) {
24
+ throw new Error(`Missing required --${name} <value> argument.`);
25
+ }
26
+ return value;
27
+ }
package/dist/bin.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { runGenerate } from './commands/generate.js';
3
+ import { runImport } from './commands/import.js';
4
+ import { runAddress } from './commands/address.js';
5
+ import { runBalance } from './commands/balance.js';
6
+ import { runSend } from './commands/send.js';
7
+ import { runCall } from './commands/call.js';
8
+ const USAGE = `hfx-wallet — local-custody HumanFX agentic wallet CLI
9
+
10
+ The private key never leaves this machine and is never printed by any command — only addresses,
11
+ balances, and tx hashes are. Register the resulting address with backend-core separately (via the
12
+ register_agentic_wallet MCP tool), this CLI has no HumanFX credential of its own.
13
+
14
+ Usage:
15
+ hfx-wallet generate [--force]
16
+ hfx-wallet import --from <file> [--force] (file must contain the raw private key)
17
+ hfx-wallet address
18
+ hfx-wallet balance [--address <0x..>] [--token <0x..>]
19
+ hfx-wallet send --to <0x..> --amount <decimal> [--token <0x..>]
20
+ hfx-wallet call --calls <file.json> ([{target, data, value?}, ...])
21
+ `;
22
+ async function main() {
23
+ const [command, ...rest] = process.argv.slice(2);
24
+ switch (command) {
25
+ case 'generate':
26
+ runGenerate(rest);
27
+ return;
28
+ case 'import':
29
+ runImport(rest);
30
+ return;
31
+ case 'address':
32
+ runAddress();
33
+ return;
34
+ case 'balance':
35
+ await runBalance(rest);
36
+ return;
37
+ case 'send':
38
+ await runSend(rest);
39
+ return;
40
+ case 'call':
41
+ await runCall(rest);
42
+ return;
43
+ default:
44
+ console.error(USAGE);
45
+ process.exit(command ? 1 : 0);
46
+ }
47
+ }
48
+ main().catch((err) => {
49
+ console.error(`Error: ${err.message}`);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,8 @@
1
+ import { createPublicClient, createWalletClient, http } from 'viem';
2
+ import { config } from './config.js';
3
+ export function getPublicClient() {
4
+ return createPublicClient({ transport: http(config.evmRpcUrl) });
5
+ }
6
+ export function getWalletClient(account) {
7
+ return createWalletClient({ account, transport: http(config.evmRpcUrl) });
8
+ }
@@ -0,0 +1,5 @@
1
+ import { loadKeystore } from '../keystore.js';
2
+ export function runAddress() {
3
+ const { address } = loadKeystore();
4
+ console.log(address);
5
+ }
@@ -0,0 +1,50 @@
1
+ import { formatUnits } from 'viem';
2
+ import { getPublicClient } from '../chain-client.js';
3
+ import { keystoreExists, loadKeystore } from '../keystore.js';
4
+ import { parseFlags } from '../args.js';
5
+ const ERC20_ABI = [
6
+ {
7
+ type: 'function',
8
+ name: 'balanceOf',
9
+ stateMutability: 'view',
10
+ inputs: [{ name: 'account', type: 'address' }],
11
+ outputs: [{ type: 'uint256' }],
12
+ },
13
+ {
14
+ type: 'function',
15
+ name: 'decimals',
16
+ stateMutability: 'view',
17
+ inputs: [],
18
+ outputs: [{ type: 'uint8' }],
19
+ },
20
+ {
21
+ type: 'function',
22
+ name: 'symbol',
23
+ stateMutability: 'view',
24
+ inputs: [],
25
+ outputs: [{ type: 'string' }],
26
+ },
27
+ ];
28
+ export async function runBalance(args) {
29
+ const flags = parseFlags(args);
30
+ const address = (typeof flags.address === 'string' ? flags.address : loadKeystoreAddressOrThrow());
31
+ const client = getPublicClient();
32
+ if (typeof flags.token === 'string') {
33
+ const token = flags.token;
34
+ const [raw, decimals, symbol] = await Promise.all([
35
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: 'balanceOf', args: [address] }),
36
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: 'decimals' }),
37
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: 'symbol' }).catch(() => ''),
38
+ ]);
39
+ console.log(`${formatUnits(raw, decimals)} ${symbol}`.trim());
40
+ return;
41
+ }
42
+ const raw = await client.getBalance({ address });
43
+ console.log(`${formatUnits(raw, 18)} (native)`);
44
+ }
45
+ function loadKeystoreAddressOrThrow() {
46
+ if (!keystoreExists()) {
47
+ throw new Error('No --address given and no local wallet found — pass --address <0x...> explicitly.');
48
+ }
49
+ return loadKeystore().address;
50
+ }
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { getWalletClient } from '../chain-client.js';
4
+ import { loadKeystore } from '../keystore.js';
5
+ import { parseFlags, requireFlag } from '../args.js';
6
+ /** Executes a list of {target, data, value} calls SEQUENTIALLY (a plain EOA can't natively batch,
7
+ * unlike a smart-contract account) — continues through the whole list even if one fails, so the
8
+ * caller gets a full per-item report rather than an opaque abort partway through. */
9
+ export async function runCall(args) {
10
+ const flags = parseFlags(args);
11
+ const callsPath = requireFlag(flags, 'calls');
12
+ const calls = JSON.parse(readFileSync(callsPath, 'utf8'));
13
+ if (!Array.isArray(calls) || calls.length === 0) {
14
+ throw new Error('--calls file must contain a non-empty JSON array of {target, data, value?}.');
15
+ }
16
+ const { private_key } = loadKeystore();
17
+ const account = privateKeyToAccount(private_key);
18
+ const wallet = getWalletClient(account);
19
+ const results = [];
20
+ for (const call of calls) {
21
+ try {
22
+ const tx_hash = await wallet.sendTransaction({
23
+ chain: null,
24
+ to: call.target,
25
+ data: call.data,
26
+ value: call.value ? BigInt(call.value) : undefined,
27
+ });
28
+ results.push({ target: call.target, ok: true, tx_hash });
29
+ }
30
+ catch (err) {
31
+ results.push({ target: call.target, ok: false, error: err.message });
32
+ }
33
+ }
34
+ console.log(JSON.stringify(results, null, 2));
35
+ }
@@ -0,0 +1,15 @@
1
+ import { generatePrivateKey } from 'viem/accounts';
2
+ import { keystoreExists, saveKeystore } from '../keystore.js';
3
+ import { config } from '../config.js';
4
+ export function runGenerate(args) {
5
+ if (keystoreExists() && !args.includes('--force')) {
6
+ throw new Error(`A wallet already exists at ${config.keyDir}. Pass --force to overwrite it (the old key ` +
7
+ `will be gone — make sure it's backed up or genuinely unneeded first).`);
8
+ }
9
+ const privateKey = generatePrivateKey();
10
+ const { address } = saveKeystore(privateKey);
11
+ console.log(`Generated new wallet.`);
12
+ console.log(`address: ${address}`);
13
+ console.log(`Private key saved to ${config.keyDir}/key.json (0600) — not printed here. Back it up ` +
14
+ 'yourself by reading that file directly outside of any agent conversation, if you want a copy.');
15
+ }
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { keystoreExists, saveKeystore } from '../keystore.js';
3
+ import { config } from '../config.js';
4
+ import { parseFlags, requireFlag } from '../args.js';
5
+ /** Reads the private key from a FILE PATH, never a direct --key value — a value passed on the
6
+ * command line lands in shell history and process listings, exactly the exposure this CLI exists
7
+ * to avoid. The file's contents are read once, copied into the keystore, and never echoed back. */
8
+ export function runImport(args) {
9
+ const flags = parseFlags(args);
10
+ if (keystoreExists() && !flags.force) {
11
+ throw new Error(`A wallet already exists at ${config.keyDir}. Pass --force to overwrite it (the old key ` +
12
+ `will be gone — make sure it's backed up or genuinely unneeded first).`);
13
+ }
14
+ const fromPath = requireFlag(flags, 'from');
15
+ const raw = readFileSync(fromPath, 'utf8').trim();
16
+ const privateKey = (raw.startsWith('0x') ? raw : `0x${raw}`);
17
+ const { address } = saveKeystore(privateKey);
18
+ console.log(`Imported wallet.`);
19
+ console.log(`address: ${address}`);
20
+ console.log(`Private key saved to ${config.keyDir}/key.json (0600) — not printed here.`);
21
+ }
@@ -0,0 +1,56 @@
1
+ import { parseUnits } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { getPublicClient, getWalletClient } from '../chain-client.js';
4
+ import { loadKeystore } from '../keystore.js';
5
+ import { parseFlags, requireFlag } from '../args.js';
6
+ const ERC20_TRANSFER_ABI = [
7
+ {
8
+ type: 'function',
9
+ name: 'transfer',
10
+ stateMutability: 'nonpayable',
11
+ inputs: [
12
+ { name: 'to', type: 'address' },
13
+ { name: 'amount', type: 'uint256' },
14
+ ],
15
+ outputs: [{ type: 'bool' }],
16
+ },
17
+ {
18
+ type: 'function',
19
+ name: 'decimals',
20
+ stateMutability: 'view',
21
+ inputs: [],
22
+ outputs: [{ type: 'uint8' }],
23
+ },
24
+ ];
25
+ export async function runSend(args) {
26
+ const flags = parseFlags(args);
27
+ const to = requireFlag(flags, 'to');
28
+ const amount = requireFlag(flags, 'amount');
29
+ const { private_key } = loadKeystore();
30
+ const account = privateKeyToAccount(private_key);
31
+ const wallet = getWalletClient(account);
32
+ const publicClient = getPublicClient();
33
+ if (typeof flags.token === 'string') {
34
+ const token = flags.token;
35
+ const decimals = await publicClient.readContract({
36
+ address: token,
37
+ abi: ERC20_TRANSFER_ABI,
38
+ functionName: 'decimals',
39
+ });
40
+ const txHash = await wallet.writeContract({
41
+ chain: null,
42
+ address: token,
43
+ abi: ERC20_TRANSFER_ABI,
44
+ functionName: 'transfer',
45
+ args: [to, parseUnits(amount, decimals)],
46
+ });
47
+ console.log(txHash);
48
+ return;
49
+ }
50
+ const txHash = await wallet.sendTransaction({
51
+ chain: null,
52
+ to,
53
+ value: parseUnits(amount, 18),
54
+ });
55
+ console.log(txHash);
56
+ }
package/dist/config.js ADDED
@@ -0,0 +1,10 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const config = {
4
+ /** Directory holding the local keystore file. Override to run multiple named wallets side by
5
+ * side, or to point at a different mount in a container. */
6
+ keyDir: process.env.HFX_WALLET_KEY_DIR ?? join(homedir(), '.humanfx', 'agent-wallet'),
7
+ /** v1: a single configured chain (worldchain, matching this protocol's own default). */
8
+ evmRpcUrl: process.env.EVM_RPC_URL ?? 'https://worldchain-mainnet.g.alchemy.com/public',
9
+ };
10
+ export const KEYSTORE_FILENAME = 'key.json';
@@ -0,0 +1,30 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { config, KEYSTORE_FILENAME } from './config.js';
5
+ function keystorePath() {
6
+ return join(config.keyDir, KEYSTORE_FILENAME);
7
+ }
8
+ export function keystoreExists() {
9
+ return existsSync(keystorePath());
10
+ }
11
+ /** Writes the keystore file with 0600 perms (0700 on its containing dir) — never logs or returns
12
+ * the private key itself; callers only ever get the address back. */
13
+ export function saveKeystore(privateKey) {
14
+ const address = privateKeyToAccount(privateKey).address;
15
+ mkdirSync(config.keyDir, { recursive: true, mode: 0o700 });
16
+ writeFileSync(keystorePath(), JSON.stringify({ private_key: privateKey, address }, null, 2), {
17
+ mode: 0o600,
18
+ });
19
+ return { address };
20
+ }
21
+ /** Loads the keystore for signing. Never printed to stdout/stderr by any command — only ever held
22
+ * in memory for the duration of a single command's execution. */
23
+ export function loadKeystore() {
24
+ if (!keystoreExists()) {
25
+ throw new Error(`No wallet found at ${keystorePath()}. Run "hfx-wallet generate" (new key) or ` +
26
+ `"hfx-wallet import --from <file>" (existing key) first.`);
27
+ }
28
+ const raw = readFileSync(keystorePath(), 'utf8');
29
+ return JSON.parse(raw);
30
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "humanfx-wallet-cli",
3
+ "version": "0.1.0",
4
+ "description": "Local-custody CLI for a HumanFX agentic wallet: generate/import a key that never leaves this machine, check balance, send tokens, execute arbitrary calldata. The private key never appears in any command's output — only addresses, balances, and tx hashes do.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "hfx-wallet": "./dist/bin.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json && chmod +x dist/bin.js",
15
+ "cli": "tsx src/bin.ts",
16
+ "lint": "eslint \"src/**/*.ts\" --max-warnings 0",
17
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run --passWithNoTests"
20
+ },
21
+ "dependencies": {
22
+ "viem": "2.23.5"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.16",
26
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
27
+ "@typescript-eslint/parser": "^6.21.0",
28
+ "eslint": "^8.56.0",
29
+ "tsx": "^4.19.2",
30
+ "typescript": "^5.3.3",
31
+ "vitest": "^2.0.5"
32
+ }
33
+ }