clawmoney 0.15.37 → 0.15.39

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.
@@ -2,7 +2,44 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { awalExec } from '../utils/awal.js';
4
4
  import { apiGet } from '../utils/api.js';
5
- import { loadConfig } from '../utils/config.js';
5
+ import { loadConfig, saveConfig } from '../utils/config.js';
6
+ // Base mainnet USDC contract + balanceOf(address) ABI selector.
7
+ // Keeping on-chain reads as a first-class path lets `wallet balance`
8
+ // skip the awal Electron bridge entirely, which is notorious for
9
+ // cold-starting slowly or hanging if the daemon isn't warm.
10
+ const BASE_RPC_URL = 'https://mainnet.base.org';
11
+ const BASE_USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
12
+ const BALANCE_OF_SELECTOR = '0x70a08231';
13
+ const USDC_DECIMALS = 1_000_000;
14
+ async function readBaseUsdcBalance(walletAddress, timeoutMs = 8000) {
15
+ const paddedAddr = walletAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0');
16
+ const data = BALANCE_OF_SELECTOR + paddedAddr;
17
+ const ctrl = new AbortController();
18
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
19
+ try {
20
+ const resp = await fetch(BASE_RPC_URL, {
21
+ method: 'POST',
22
+ headers: { 'content-type': 'application/json' },
23
+ body: JSON.stringify({
24
+ jsonrpc: '2.0',
25
+ id: 1,
26
+ method: 'eth_call',
27
+ params: [{ to: BASE_USDC_CONTRACT, data }, 'latest'],
28
+ }),
29
+ signal: ctrl.signal,
30
+ });
31
+ const json = (await resp.json());
32
+ if (json.error)
33
+ throw new Error(json.error.message || 'RPC error');
34
+ if (!json.result)
35
+ throw new Error('empty RPC result');
36
+ const atomic = BigInt(json.result);
37
+ return Number(atomic) / USDC_DECIMALS;
38
+ }
39
+ finally {
40
+ clearTimeout(timer);
41
+ }
42
+ }
6
43
  export async function walletStatusCommand() {
7
44
  const spinner = ora('Getting wallet status...').start();
8
45
  try {
@@ -39,47 +76,94 @@ function withTimeout(p, ms, label) {
39
76
  }
40
77
  export async function walletBalanceCommand() {
41
78
  const spinner = ora('Getting wallet balance...').start();
42
- // Kick off both calls in parallel. Relay earnings are loaded from
43
- // the clawmoney backend per-agent; the on-chain balance is awal's
44
- // native `balance` RPC. Either half is allowed to fail we print
45
- // a dim "(unavailable)" note for that section and keep going.
79
+ // Source of truth for on-chain balance: direct JSON-RPC to Base
80
+ // mainnet USDC, not `awal balance`. Reasons:
81
+ // - awal is an Electron app; cold-starting it via `npx` takes 3-10s
82
+ // and occasionally wedges under load (see GH issues around
83
+ // DEP0190 / pipe buffers).
84
+ // - We already store the wallet address in ~/.clawmoney/config.yaml
85
+ // at setup time, so we don't need awal to look it up.
86
+ // - RPC reads are idempotent, cacheable, and cost nothing.
87
+ // Relay earnings are fetched in parallel from the clawmoney backend.
88
+ // Either half is allowed to fail — we print "(unavailable)" for the
89
+ // broken section and keep going.
46
90
  const config = loadConfig();
91
+ // Wallet address lookup order:
92
+ // 1. ~/.clawmoney/config.yaml cache (instant)
93
+ // 2. Backend /api/v1/claw-agents/me (authoritative, ~200ms)
94
+ // 3. awal address (Electron cold-start, last resort, 5s cap)
95
+ // After a successful #2 we write the result back to the config so
96
+ // every future `wallet balance` hits path #1.
97
+ let walletAddress = config?.wallet_address ?? null;
98
+ let addressSource = 'config';
99
+ let addressError = null;
100
+ if (!walletAddress && config?.api_key) {
101
+ try {
102
+ const resp = await apiGet('/api/v1/claw-agents/me', config.api_key);
103
+ if (resp.ok && typeof resp.data?.wallet_address === 'string' && resp.data.wallet_address) {
104
+ walletAddress = resp.data.wallet_address;
105
+ addressSource = 'api';
106
+ // Cache it so the next run is instant.
107
+ try {
108
+ saveConfig({ wallet_address: walletAddress });
109
+ }
110
+ catch {
111
+ // Non-fatal — we still have the address for THIS run.
112
+ }
113
+ }
114
+ }
115
+ catch (err) {
116
+ addressError = err.message;
117
+ }
118
+ }
119
+ if (!walletAddress) {
120
+ // Last resort: cold-start awal. Capped at 5s so a wedged wallet
121
+ // daemon can't block the command forever.
122
+ try {
123
+ const awalResult = await withTimeout(awalExec(['address']), 5_000, 'awal address');
124
+ const data = awalResult.data;
125
+ if (typeof data?.address === 'string' && data.address) {
126
+ walletAddress = data.address;
127
+ addressSource = 'awal';
128
+ }
129
+ }
130
+ catch (err) {
131
+ addressError = err.message;
132
+ }
133
+ }
47
134
  const relayPromise = config?.api_key
48
135
  ? apiGet("/api/v1/relay/providers/me", config.api_key)
49
136
  .then((resp) => (resp.ok && Array.isArray(resp.data) ? resp.data : null))
50
137
  .catch(() => null)
51
138
  : Promise.resolve(null);
52
- // 10s is generous — awal balance usually returns in 1-2s. Beyond
53
- // that the wallet process is probably wedged and the user wants
54
- // the rest of the output immediately.
55
- let awalResult = null;
56
- let awalError = null;
57
- try {
58
- awalResult = await withTimeout(awalExec(['balance']), 10_000, 'awal balance');
139
+ let usdcBalance = null;
140
+ let onchainError = null;
141
+ if (walletAddress) {
142
+ try {
143
+ usdcBalance = await readBaseUsdcBalance(walletAddress);
144
+ }
145
+ catch (err) {
146
+ onchainError = err.message;
147
+ }
59
148
  }
60
- catch (err) {
61
- awalError = err.message;
149
+ else {
150
+ onchainError = addressError ?? 'no wallet address in config';
62
151
  }
152
+ void addressSource; // reserved for future debug display
63
153
  const relayRows = await relayPromise;
64
154
  spinner.stop();
65
155
  console.log('');
66
156
  console.log(chalk.bold(' Wallet'));
67
157
  console.log('');
68
- console.log(chalk.bold(' On-chain (awal)'));
69
- if (awalResult) {
70
- const data = awalResult.data;
71
- if (typeof data === 'object' && data !== null && Object.keys(data).length > 0) {
72
- for (const [key, value] of Object.entries(data)) {
73
- console.log(` ${chalk.dim(key + ':').padEnd(22)} ${chalk.green(String(value))}`);
74
- }
75
- }
76
- else {
77
- console.log(` ${chalk.dim(awalResult.raw || '(empty)')}`);
78
- }
158
+ console.log(chalk.bold(' On-chain (Base USDC)'));
159
+ if (walletAddress) {
160
+ console.log(` ${chalk.dim('Address:').padEnd(22)} ${chalk.cyan(walletAddress)}`);
161
+ }
162
+ if (usdcBalance !== null) {
163
+ console.log(` ${chalk.dim('USDC:').padEnd(22)} ${chalk.green('$' + usdcBalance.toFixed(2))}`);
79
164
  }
80
165
  else {
81
- console.log(` ${chalk.yellow('unavailable')} ${chalk.dim('(' + (awalError ?? 'unknown error') + ')')}`);
82
- console.log(chalk.dim(' Try: npx awal status | clawmoney wallet status'));
166
+ console.log(` ${chalk.yellow('unavailable')} ${chalk.dim('(' + (onchainError ?? 'unknown') + ')')}`);
83
167
  }
84
168
  console.log('');
85
169
  console.log(chalk.bold(' Relay earnings'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.37",
3
+ "version": "0.15.39",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {