Skip to content

Commit 4a711e0

Browse files
MajorTalclaude
andcommitted
Use atomic writes for wallet storage, enhance balance command
saveWallet now writes to a temp file and renames into place to prevent corruption on crash. Balance command shows on-chain USDC for both Base Mainnet and Sepolia alongside the Run402 billing balance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93f3e8a commit 4a711e0

2 files changed

Lines changed: 41 additions & 12 deletions

File tree

cli/lib/config.mjs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* Kept in a separate module so credential reads stay isolated.
44
*/
55

6-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
7-
import { join } from "path";
6+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, renameSync } from "fs";
7+
import { join, dirname } from "path";
88
import { homedir } from "os";
9+
import { randomBytes } from "crypto";
910

1011
export const CONFIG_DIR = join(homedir(), ".config", "run402");
1112
export const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
@@ -19,8 +20,10 @@ export function readWallet() {
1920

2021
export function saveWallet(data) {
2122
mkdirSync(CONFIG_DIR, { recursive: true });
22-
writeFileSync(WALLET_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
23-
try { chmodSync(WALLET_FILE, 0o600); } catch {}
23+
const tmp = join(CONFIG_DIR, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
24+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
25+
renameSync(tmp, WALLET_FILE);
26+
chmodSync(WALLET_FILE, 0o600);
2427
}
2528

2629
export function loadProjects() {

cli/lib/wallet.mjs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Subcommands:
99
status Show wallet address, network, and funding status
1010
create Generate a new wallet and save it locally
1111
fund Request test USDC from the Run402 faucet (Base Sepolia)
12-
balance Check billing balance for this wallet
12+
balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
1313
export Print the wallet address (useful for scripting)
1414
1515
Notes:
@@ -24,11 +24,15 @@ Examples:
2424
run402 wallet export
2525
`;
2626

27+
const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
28+
const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
29+
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
30+
2731
async function loadDeps() {
2832
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
2933
const { createPublicClient, http } = await import("viem");
30-
const { baseSepolia } = await import("viem/chains");
31-
return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, baseSepolia };
34+
const { base, baseSepolia } = await import("viem/chains");
35+
return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia };
3236
}
3337

3438
async function status() {
@@ -37,7 +41,7 @@ async function status() {
3741
console.log(JSON.stringify({ status: "no_wallet", message: "No wallet found. Run: run402 wallet create" }));
3842
return;
3943
}
40-
console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false }));
44+
console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false, path: WALLET_FILE }));
4145
}
4246

4347
async function create() {
@@ -66,13 +70,35 @@ async function fund() {
6670
}
6771
}
6872

73+
async function readUsdcBalance(client, usdc, address) {
74+
const raw = await client.readContract({ address: usdc, abi: USDC_ABI, functionName: "balanceOf", args: [address] });
75+
return Number(raw) / 1e6;
76+
}
77+
6978
async function balance() {
7079
const w = readWallet();
7180
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
72-
const res = await fetch(`${API}/v1/billing/accounts/${w.address.toLowerCase()}`);
73-
const data = await res.json();
74-
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
75-
console.log(JSON.stringify(data, null, 2));
81+
82+
const { createPublicClient, http, base, baseSepolia } = await loadDeps();
83+
const mainnetClient = createPublicClient({ chain: base, transport: http() });
84+
const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
85+
86+
const [mainnetUsdc, sepoliaUsdc, billingRes] = await Promise.all([
87+
readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
88+
readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
89+
fetch(`${API}/v1/billing/accounts/${w.address.toLowerCase()}`),
90+
]);
91+
92+
const billing = billingRes.ok ? await billingRes.json() : null;
93+
94+
console.log(JSON.stringify({
95+
address: w.address,
96+
onchain: {
97+
"base-mainnet": mainnetUsdc !== null ? `${mainnetUsdc} USDC` : "unavailable",
98+
"base-sepolia": sepoliaUsdc !== null ? `${sepoliaUsdc} USDC` : "unavailable",
99+
},
100+
run402: billing || "no billing account",
101+
}, null, 2));
76102
}
77103

78104
async function exportAddr() {

0 commit comments

Comments
 (0)