Skip to content

Commit b4a2782

Browse files
committed
feat: harden wallet backups and mining flow
1 parent 8417d17 commit b4a2782

12 files changed

Lines changed: 381 additions & 187 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,16 @@ If you want to control each step manually, the older `setup -> fund -> mint -> m
8383
| `apow start` | Guided happy path: setup -> fund -> mint -> mine |
8484
| `apow setup` | Agent-first setup wizard: Easy Mode (x402 everywhere) or Advanced Mode |
8585
| `apow fund` | Fund your wallet: bridge from Solana/Ethereum or send on Base, auto-split ETH+USDC |
86-
| `apow wallet new` | Generate a new mining wallet |
86+
| `apow wallet new` | Generate a new mining wallet, plus optional encrypted JSON keystore backup |
8787
| `apow wallet show` | Show configured wallet address |
88-
| `apow wallet export` | Export your wallet's private key |
88+
| `apow wallet export` | Export your wallet's private key and optional backups |
8989
| `apow wallet fund <addr> [eth]` | Send ETH to another address (default: mint price + gas) |
9090
| `apow mint` | Mint a MiningAgent NFT (one per wallet) |
9191
| `apow mine [tokenId]` | Mine $AGENT with your NFT (auto-detects best rig) |
9292
| `apow stats [tokenId]` | View mining stats, earnings, difficulty |
9393
| `apow dashboard start` | Launch multi-wallet mining dashboard |
9494
| `apow dashboard add <addr>` | Add a wallet to the dashboard |
95-
| `apow dashboard scan [dir]` | Auto-detect wallet files in a directory |
95+
| `apow dashboard scan [dir]` | Auto-detect `wallet-0x*.txt` and `wallet-0x*.json` files in a directory |
9696

9797
## Configuration
9898

@@ -107,6 +107,7 @@ ALLOW_LOCAL_FALLBACK_WITH_X402=false # Easy Mode default: do not burn local CPU
107107
# LLM_PROVIDER=clawrouter # clawrouter (auto with x402) | openai | gemini | deepseek | qwen | anthropic | ollama (for minting)
108108
# LLM_MODEL=blockrun/eco # Auto-detected per provider; override only if needed
109109
# LLM_API_KEY=sk-... # Not needed with clawrouter/ollama; required for openai/gemini/etc.
110+
# KEYSTORE_PASSWORD=... # Optional: create encrypted wallet JSON backups during wallet new/export
110111
# Bridging (only for `apow fund`)
111112
# SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
112113
# ETHEREUM_RPC_URL=https://cloudflare-eth.com
@@ -219,7 +220,7 @@ Monitor your entire mining fleet from a single web UI. Zero external dependencie
219220

220221
```bash
221222
# Quick start: scan wallet files and launch
222-
apow dashboard scan . # detect wallet-0x*.txt files in current directory
223+
apow dashboard scan . # detect wallet-0x*.txt / .json files in current directory
223224
apow dashboard start # open dashboard at http://localhost:3847
224225
```
225226

@@ -230,7 +231,7 @@ apow dashboard start # open dashboard at http://localhost:3847
230231
| `apow dashboard start` | Launch dashboard web UI (default port 3847) |
231232
| `apow dashboard add <addr>` | Add a wallet address to monitor |
232233
| `apow dashboard remove <addr>` | Remove a wallet from monitoring |
233-
| `apow dashboard scan [dir]` | Auto-detect wallets from `wallet-0x*.txt` files |
234+
| `apow dashboard scan [dir]` | Auto-detect wallets from `wallet-0x*.txt` and `wallet-0x*.json` files |
234235
| `apow dashboard wallets` | List all monitored wallets |
235236

236237
### Fleet Configuration
@@ -245,7 +246,7 @@ Wallets are stored in `~/.apow/wallets.json` (plain JSON array of addresses). Fo
245246
]
246247
```
247248

248-
Fleet types: `array` (JSON array of addresses), `solkek` (master/miners JSON), `rigdirs` (scan `rig*/wallet-0x*.txt`), `walletfiles` (scan `wallet-0x*.txt`).
249+
Fleet types: `array` (JSON array of addresses), `solkek` (master/miners JSON), `rigdirs` (scan `rig*/wallet-0x*.txt` / `.json`), `walletfiles` (scan `wallet-0x*.txt` / `.json`).
249250

250251
## Protocol
251252

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apow-cli",
3-
"version": "0.11.5",
3+
"version": "0.11.6",
44
"description": "Mine AGENT tokens on Base L2 with Agentic Proof of Work",
55
"keywords": [
66
"apow",

skill.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ Or generate one directly (useful for agents, no prompts):
134134
npx apow-cli wallet new
135135
```
136136

137-
This outputs a private key (0x + 64 hex chars) and Base address, and saves a `wallet-<address>.txt` file to the current directory. The private key goes in your `.env` as `PRIVATE_KEY`.
137+
This outputs a private key (0x + 64 hex chars) and Base address, saves a plaintext `wallet-<address>.txt` import helper to the current directory, and can also create an encrypted `wallet-<address>.json` keystore under `~/.apow/keystores/` when a password is available. The private key goes in your `.env` as `PRIVATE_KEY`.
138138

139139
**Exporting an existing wallet:** If you've already set up a wallet and need to retrieve the key:
140140

141141
```bash
142142
npx apow-cli wallet export
143143
```
144144

145-
This prompts for confirmation, then displays your address and private key. It also offers to save a `wallet-<address>.txt` file if one doesn't already exist.
145+
This prompts for confirmation, then displays your address and private key. It can save a plaintext `wallet-<address>.txt` import helper and/or an encrypted JSON keystore backup.
146146

147147
**Exporting to a wallet app:** The user can import this private key into Phantom, MetaMask, Rainbow, or any EVM-compatible wallet to view their AGENT tokens and Mining Rig NFT alongside their other assets.
148148

@@ -263,6 +263,7 @@ CHAIN=base
263263
| Variable | Required | Default | Description |
264264
|---|---|---|---|
265265
| `PRIVATE_KEY` | Yes | - | Wallet private key (0x + 64 hex chars) |
266+
| `KEYSTORE_PASSWORD` | No | unset | If set, `wallet new`/`wallet export` also create an encrypted JSON keystore backup under `~/.apow/keystores/` |
266267
| `MINING_AGENT_ADDRESS` | Yes | - | Deployed MiningAgent contract address |
267268
| `AGENT_COIN_ADDRESS` | Yes | - | Deployed AgentCoin contract address |
268269
| `LLM_PROVIDER` | For minting | `clawrouter` if `USE_X402=true`, else `openai` | LLM provider for minting: `clawrouter` (recommended, zero credentials), `openai`, `gemini`, `deepseek`, `qwen`, `anthropic`, `ollama`, `claude-code`, `codex`. Not needed for mining. |
@@ -650,7 +651,7 @@ This section addresses the security model of apow-cli head-on. Every claim below
650651
651652
### Private Key Generation (Local Only)
652653
653-
Keys are generated via `viem/accounts` `generatePrivateKey()`, which uses Node.js `crypto.randomBytes(32)`, a cryptographically secure random number generator. Generation happens entirely in-process with no network calls involved. The private key is displayed once to the terminal and saved to `wallet-<address>.txt` with file permissions `0o600` (owner-read-write only).
654+
Keys are generated via `viem/accounts` `generatePrivateKey()`, which uses Node.js `crypto.randomBytes(32)`, a cryptographically secure random number generator. Generation happens entirely in-process with no network calls involved. The private key is displayed once to the terminal, and the CLI can create both a plaintext `wallet-<address>.txt` import helper and a password-protected JSON keystore (`Web3 Secret Storage v3`) with file permissions `0o600`.
654655
655656
### Private Key Is NEVER Transmitted
656657
@@ -698,7 +699,7 @@ The SMHL solver sends only generic word-generation prompts to the LLM (e.g., "Wr
698699
699700
1. **Use a fresh wallet.** Generate one with `npx apow-cli wallet new`. Do not import your main wallet or any wallet holding significant funds.
700701
2. **Fund with only what you need.** ~0.005 ETH covers minting + several mining cycles.
701-
3. **Wallet backups are created automatically** at `wallet-<address>.txt` with restricted file permissions (`0o600`).
702+
3. **Prefer encrypted keystore backups** in `~/.apow/keystores/wallet-<address>.json`. The plaintext `wallet-<address>.txt` helper is convenient for importing into wallet UIs, but it is less secure.
702703
4. **Verify the source before running** if you prefer:
703704
```bash
704705
git clone https://github.com/Agentoshi/apow-cli
@@ -769,7 +770,7 @@ The `apow dashboard` command group provides a real-time web UI for monitoring yo
769770
| `apow dashboard start` | Launch the dashboard web UI at `http://localhost:3847`. Auto-opens browser. Press Ctrl+C to stop. |
770771
| `apow dashboard add <address>` | Add a wallet address to monitor. Validates 0x + 40 hex chars. |
771772
| `apow dashboard remove <address>` | Remove a wallet address from monitoring. |
772-
| `apow dashboard scan [dir]` | Auto-detect wallets from `wallet-0x*.txt` files in the given directory (default: CWD). Also scans `rig*/` subdirectories. |
773+
| `apow dashboard scan [dir]` | Auto-detect wallets from `wallet-0x*.txt` and `wallet-0x*.json` files in the given directory (default: CWD). Also scans `rig*/` subdirectories. |
773774
| `apow dashboard wallets` | List all currently monitored wallet addresses. |
774775
775776
### How It Works
@@ -779,7 +780,7 @@ The `apow dashboard` command group provides a real-time web UI for monitoring yo
779780
- **Data fetching:** Chunked RPC multicalls (max 30 per batch) with a 25-second TTL cache. Queries ETH balance, AGENT balance, rig ownership, rarity, hashpower, mine count, and earnings for every wallet.
780781
- **NFT art:** Renders on-chain SVG art for each Mining Rig with rarity-based color coding.
781782
- **Auto-seed:** On first run, seeds `wallets.json` with the address from your `.env` if configured.
782-
- **Auto-detect:** `dashboard start` automatically scans CWD for `wallet-0x*.txt` files before launching.
783+
- **Auto-detect:** `dashboard start` automatically scans CWD for `wallet-0x*.txt` and `wallet-0x*.json` files before launching.
783784
784785
### Fleet Configuration (`~/.apow/fleets.json`)
785786
@@ -800,8 +801,8 @@ For managing wallets across multiple machines or directories, create `~/.apow/fl
800801
|------|--------------|-------------|
801802
| `array` | JSON array of addresses | Simple list: `["0xABC...", "0xDEF..."]` |
802803
| `solkek` | JSON with `master.address` + `miners[].address` | Solkek fleet manager format |
803-
| `rigdirs` | Directory containing `rig*/wallet-0x*.txt` | Scan rig subdirectories for wallet files |
804-
| `walletfiles` | Directory containing `wallet-0x*.txt` | Scan flat directory for wallet files |
804+
| `rigdirs` | Directory containing `rig*/wallet-0x*.txt` or `.json` | Scan rig subdirectories for wallet files |
805+
| `walletfiles` | Directory containing `wallet-0x*.txt` or `.json` | Scan flat directory for wallet files |
805806
806807
If `fleets.json` does not exist, the dashboard falls back to `wallets.json` as a single "Main" fleet.
807808

src/config.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { config as loadEnv } from "dotenv";
2-
import { writeFile } from "node:fs/promises";
2+
import { existsSync } from "node:fs";
3+
import { readFile, writeFile } from "node:fs/promises";
34
import os from "node:os";
45
import { join } from "node:path";
56
import type { Address, Chain, Hex } from "viem";
@@ -203,10 +204,34 @@ export function isExpensiveModel(model: string): boolean {
203204
}
204205

205206
export async function writeEnvFile(values: Record<string, string>): Promise<void> {
206-
const lines = Object.entries(values)
207-
.map(([key, value]) => `${key}=${value}`)
208-
.join("\n");
209-
await writeFile(join(process.cwd(), ".env"), lines + "\n", "utf8");
207+
const envPath = join(process.cwd(), ".env");
208+
const preserved: string[] = [];
209+
210+
if (existsSync(envPath)) {
211+
const existingContent = await readFile(envPath, "utf8").catch(() => "");
212+
const seenKeys = new Set(Object.keys(values));
213+
for (const line of existingContent.split(/\r?\n/)) {
214+
const trimmed = line.trim();
215+
if (!trimmed || trimmed.startsWith("#")) {
216+
continue;
217+
}
218+
const eq = line.indexOf("=");
219+
if (eq === -1) {
220+
preserved.push(line);
221+
continue;
222+
}
223+
const key = line.slice(0, eq).trim();
224+
if (!seenKeys.has(key)) {
225+
preserved.push(line);
226+
}
227+
}
228+
}
229+
230+
const lines = [
231+
...preserved,
232+
...Object.entries(values).map(([key, value]) => `${key}=${value}`),
233+
].join("\n");
234+
await writeFile(envPath, lines + "\n", "utf8");
210235
for (const [key, value] of Object.entries(values)) {
211236
process.env[key] = value;
212237
}

src/dashboard.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createPublicClient, formatEther, http as viemHttp, type Abi, type Addre
55
import { base } from "viem/chains";
66
import { getDashboardHtml } from "./dashboard-html";
77
import { createX402Transport } from "./x402";
8+
import { detectWalletAddressFromFilename } from "./wallet-store";
89

910
import AgentCoinAbiJson from "./abi/AgentCoin.json";
1011
import MiningAgentAbiJson from "./abi/MiningAgent.json";
@@ -96,9 +97,9 @@ function extractRigdirs(dir: string): Address[] {
9697
if (!entry.isDirectory() || !entry.name.startsWith("rig")) continue;
9798
const rigFiles = readdirSync(join(dir, entry.name));
9899
for (const file of rigFiles) {
99-
const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
100-
if (match && isAddress(match[1])) {
101-
addrs.push(match[1] as Address);
100+
const address = detectWalletAddressFromFilename(file);
101+
if (address && isAddress(address)) {
102+
addrs.push(address as Address);
102103
}
103104
}
104105
}
@@ -109,9 +110,9 @@ function extractWalletfiles(dir: string): Address[] {
109110
const addrs: Address[] = [];
110111
const files = readdirSync(dir);
111112
for (const file of files) {
112-
const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
113-
if (match && isAddress(match[1])) {
114-
addrs.push(match[1] as Address);
113+
const address = detectWalletAddressFromFilename(file);
114+
if (address && isAddress(address)) {
115+
addrs.push(address as Address);
115116
}
116117
}
117118
return addrs;

src/detect.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ export async function detectMinersWithClient(client: PublicClient, owner: Addres
2626
return [];
2727
}
2828

29-
const miners: OwnedMiner[] = [];
30-
31-
for (let i = 0n; i < balance; i++) {
32-
const tokenId = (await client.readContract({
29+
const indexes = Array.from({ length: Number(balance) }, (_, i) => BigInt(i));
30+
const tokenIds = await Promise.all(indexes.map((index) =>
31+
client.readContract({
3332
address: config.miningAgentAddress,
3433
abi: miningAgentAbi,
3534
functionName: "tokenOfOwnerByIndex",
36-
args: [owner, i],
37-
})) as bigint;
35+
args: [owner, index],
36+
}) as Promise<bigint>,
37+
));
3838

39+
const miners = await Promise.all(tokenIds.map(async (tokenId) => {
3940
const [rarityRaw, hashpowerRaw] = await Promise.all([
4041
client.readContract({
4142
address: config.miningAgentAddress,
@@ -52,13 +53,13 @@ export async function detectMinersWithClient(client: PublicClient, owner: Addres
5253
]);
5354

5455
const rarity = Number(rarityRaw);
55-
miners.push({
56+
return {
5657
tokenId,
5758
rarity,
5859
rarityLabel: rarityLabels[rarity] ?? `Tier ${rarity}`,
5960
hashpower: Number(hashpowerRaw),
60-
});
61-
}
61+
};
62+
}));
6263

6364
return miners;
6465
}

src/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@ const patterns: Array<{
118118
}),
119119
},
120120
{
121-
test: (m) => m.toLowerCase().includes("insufficient funds"),
121+
test: (m) => m.includes("Not enough ETH for gas") || m.toLowerCase().includes("insufficient funds"),
122122
classify: () => ({
123123
category: "setup",
124124
userMessage: "Not enough ETH for gas",
125-
recovery: "Send ETH to your wallet on Base",
125+
recovery: "Send ETH to your wallet on Base, or run `apow fund` to top up gas before mining again",
126126
}),
127127
},
128128
{

src/fund.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,10 @@ export async function runFundFlow(options: FundOptions): Promise<void> {
593593
console.log(` Next: ${ui.cyan("apow mint")}`);
594594
console.log("");
595595

596+
if (!ui.isInteractiveSession()) {
597+
return;
598+
}
599+
596600
// Allow explicit re-run if user wants to add more
597601
const addMore = await ui.confirm("Add more funds?");
598602
if (!addMore) return;

0 commit comments

Comments
 (0)