forked from BlockRunAI/ClawRouter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-balance.ts
More file actions
299 lines (254 loc) · 10.9 KB
/
Copy pathtest-balance.ts
File metadata and controls
299 lines (254 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
/**
* Unit tests for balance monitoring feature.
*
* Tests:
* 1. BalanceMonitor formatting and thresholds
* 2. Error classes and type guards
* 3. Integration with proxy (using mock RPC)
*
* Usage:
* npx tsx test-balance.ts
*/
import { BalanceMonitor, BALANCE_THRESHOLDS, type BalanceInfo } from "./src/balance.js";
import {
InsufficientFundsError,
EmptyWalletError,
RpcError,
isInsufficientFundsError,
isEmptyWalletError,
isBalanceError,
isRpcError,
} from "./src/errors.js";
let passed = 0;
let failed = 0;
function test(name: string, fn: () => void | Promise<void>) {
const run = async () => {
process.stdout.write(` ${name} ... `);
try {
await fn();
console.log("PASS");
passed++;
} catch (err) {
console.log("FAIL");
console.error(` ${err instanceof Error ? err.message : String(err)}`);
failed++;
}
};
return run();
}
function assert(condition: boolean, msg: string) {
if (!condition) throw new Error(msg);
}
function assertEqual<T>(actual: T, expected: T, msg?: string) {
if (actual !== expected) {
throw new Error(msg || `Expected ${expected}, got ${actual}`);
}
}
async function main() {
console.log("\n=== Balance Monitoring Tests ===\n");
// --- Error Classes ---
console.log("Error Classes:");
await test("InsufficientFundsError has correct properties", () => {
const err = new InsufficientFundsError({
currentBalanceUSD: "$0.50",
requiredUSD: "$1.00",
walletAddress: "0x1234",
});
assertEqual(err.code, "INSUFFICIENT_FUNDS");
assertEqual(err.currentBalanceUSD, "$0.50");
assertEqual(err.requiredUSD, "$1.00");
assertEqual(err.walletAddress, "0x1234");
assert(err.message.includes("$0.50"), "Message should include current balance");
assert(err.message.includes("$1.00"), "Message should include required amount");
assert(err.message.includes("0x1234"), "Message should include wallet address");
});
await test("EmptyWalletError has correct properties", () => {
const err = new EmptyWalletError("0xABCD");
assertEqual(err.code, "EMPTY_WALLET");
assertEqual(err.walletAddress, "0xABCD");
assert(err.message.includes("0xABCD"), "Message should include wallet address");
assert(err.message.includes("No USDC"), "Message should mention no balance");
});
await test("isInsufficientFundsError type guard works", () => {
const insuffErr = new InsufficientFundsError({
currentBalanceUSD: "$0",
requiredUSD: "$1",
walletAddress: "0x",
});
const emptyErr = new EmptyWalletError("0x");
const genericErr = new Error("generic");
assert(isInsufficientFundsError(insuffErr), "Should detect InsufficientFundsError");
assert(
!isInsufficientFundsError(emptyErr),
"Should not detect EmptyWalletError as InsufficientFundsError",
);
assert(!isInsufficientFundsError(genericErr), "Should not detect generic Error");
assert(!isInsufficientFundsError(null), "Should handle null");
assert(!isInsufficientFundsError("string"), "Should handle string");
});
await test("isEmptyWalletError type guard works", () => {
const insuffErr = new InsufficientFundsError({
currentBalanceUSD: "$0",
requiredUSD: "$1",
walletAddress: "0x",
});
const emptyErr = new EmptyWalletError("0x");
assert(isEmptyWalletError(emptyErr), "Should detect EmptyWalletError");
assert(
!isEmptyWalletError(insuffErr),
"Should not detect InsufficientFundsError as EmptyWalletError",
);
});
await test("isBalanceError detects both error types", () => {
const insuffErr = new InsufficientFundsError({
currentBalanceUSD: "$0",
requiredUSD: "$1",
walletAddress: "0x",
});
const emptyErr = new EmptyWalletError("0x");
const genericErr = new Error("generic");
assert(isBalanceError(insuffErr), "Should detect InsufficientFundsError");
assert(isBalanceError(emptyErr), "Should detect EmptyWalletError");
assert(!isBalanceError(genericErr), "Should not detect generic Error");
});
// --- RpcError ---
console.log("\nRpcError:");
await test("RpcError has correct properties", () => {
const originalErr = new Error("connection refused");
const err = new RpcError("Failed to connect to Base RPC", originalErr);
assertEqual(err.code, "RPC_ERROR");
assertEqual(err.originalError, originalErr);
assert(
err.message.includes("Failed to connect to Base RPC"),
"Message should include error details",
);
assert(err.message.includes("Check network connectivity"), "Message should include help text");
});
await test("RpcError without original error", () => {
const err = new RpcError("Unknown RPC failure");
assertEqual(err.code, "RPC_ERROR");
assertEqual(err.originalError, undefined);
});
await test("isRpcError type guard works", () => {
const rpcErr = new RpcError("test");
const emptyErr = new EmptyWalletError("0x");
const genericErr = new Error("generic");
assert(isRpcError(rpcErr), "Should detect RpcError");
assert(!isRpcError(emptyErr), "Should not detect EmptyWalletError as RpcError");
assert(!isRpcError(genericErr), "Should not detect generic Error");
assert(!isRpcError(null), "Should handle null");
assert(!isRpcError("string"), "Should handle string");
});
await test("RpcError is not a balance error", () => {
const rpcErr = new RpcError("test");
assert(!isBalanceError(rpcErr), "RpcError should not be detected as balance error");
});
// --- BalanceMonitor ---
console.log("\nBalanceMonitor:");
await test("formatUSDC formats correctly", () => {
// Create monitor with dummy address (won't actually call RPC in these tests)
const monitor = new BalanceMonitor("0x0000000000000000000000000000000000000000");
assertEqual(monitor.formatUSDC(0n), "$0.00");
assertEqual(monitor.formatUSDC(1n), "$0.00"); // rounds down
assertEqual(monitor.formatUSDC(100n), "$0.00"); // $0.0001
assertEqual(monitor.formatUSDC(1000n), "$0.00"); // $0.001
assertEqual(monitor.formatUSDC(10000n), "$0.01"); // $0.01
assertEqual(monitor.formatUSDC(100000n), "$0.10"); // $0.10
assertEqual(monitor.formatUSDC(1000000n), "$1.00"); // $1.00
assertEqual(monitor.formatUSDC(1500000n), "$1.50"); // $1.50
assertEqual(monitor.formatUSDC(12345678n), "$12.35"); // $12.345678 rounds
assertEqual(monitor.formatUSDC(100000000n), "$100.00"); // $100.00
});
await test("BALANCE_THRESHOLDS are correct", () => {
assertEqual(BALANCE_THRESHOLDS.LOW_BALANCE_MICROS, 1_000_000n, "Low balance should be $1.00");
assertEqual(BALANCE_THRESHOLDS.ZERO_THRESHOLD, 100n, "Zero threshold should be $0.0001");
});
await test("getWalletAddress returns correct address", () => {
const addr = "0x1234567890abcdef1234567890abcdef12345678";
const monitor = new BalanceMonitor(addr);
assertEqual(monitor.getWalletAddress(), addr);
});
// --- Balance Info Building (indirect test via checkSufficient with mocked balance) ---
console.log("\nBalance Thresholds:");
await test("Balance < $0.0001 is considered empty", () => {
// Test the threshold logic
const balance = 50n; // $0.00005
const isEmpty = balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD;
assert(isEmpty, "Balance of $0.00005 should be empty");
});
await test("Balance >= $0.0001 is not empty", () => {
const balance = 100n; // $0.0001
const isEmpty = balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD;
assert(!isEmpty, "Balance of $0.0001 should not be empty");
});
await test("Balance < $1.00 is considered low", () => {
const balance = 999_999n; // $0.999999
const isLow = balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS;
assert(isLow, "Balance of $0.999999 should be low");
});
await test("Balance >= $1.00 is not low", () => {
const balance = 1_000_000n; // $1.00
const isLow = balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS;
assert(!isLow, "Balance of $1.00 should not be low");
});
// --- Cache behavior ---
console.log("\nCache Behavior:");
await test("deductEstimated reduces cached balance", () => {
const monitor = new BalanceMonitor("0x0000000000000000000000000000000000000000");
// Manually set cache for testing (access private via any)
(monitor as any).cachedBalance = 5_000_000n; // $5.00
(monitor as any).cachedAt = Date.now();
monitor.deductEstimated(1_000_000n); // deduct $1.00
assertEqual((monitor as any).cachedBalance, 4_000_000n, "Should have $4.00 after deduction");
monitor.deductEstimated(500_000n); // deduct $0.50
assertEqual(
(monitor as any).cachedBalance,
3_500_000n,
"Should have $3.50 after second deduction",
);
});
await test("deductEstimated does not go negative", () => {
const monitor = new BalanceMonitor("0x0000000000000000000000000000000000000000");
(monitor as any).cachedBalance = 500_000n; // $0.50
(monitor as any).cachedAt = Date.now();
monitor.deductEstimated(1_000_000n); // try to deduct $1.00
// Should not deduct if balance < amount
assertEqual((monitor as any).cachedBalance, 500_000n, "Should not deduct if insufficient");
});
await test("invalidate clears cache", () => {
const monitor = new BalanceMonitor("0x0000000000000000000000000000000000000000");
(monitor as any).cachedBalance = 5_000_000n;
(monitor as any).cachedAt = Date.now();
monitor.invalidate();
assertEqual((monitor as any).cachedBalance, null, "Cache should be null after invalidate");
assertEqual((monitor as any).cachedAt, 0, "Cache timestamp should be 0 after invalidate");
});
// --- Live RPC test (optional, requires network) ---
console.log("\nLive RPC Test:");
await test("checkBalance fetches from Base RPC", async () => {
// Use a known address (USDC contract itself has 0 USDC)
const monitor = new BalanceMonitor("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
try {
const info = await monitor.checkBalance();
assert(typeof info.balance === "bigint", "Balance should be bigint");
assert(typeof info.balanceUSD === "string", "balanceUSD should be string");
assert(info.balanceUSD.startsWith("$"), "balanceUSD should start with $");
assert(typeof info.isLow === "boolean", "isLow should be boolean");
assert(typeof info.isEmpty === "boolean", "isEmpty should be boolean");
assert(info.walletAddress.startsWith("0x"), "walletAddress should start with 0x");
console.log(`(balance: ${info.balanceUSD}, isLow: ${info.isLow}, isEmpty: ${info.isEmpty})`);
} catch (err) {
// RPC might fail in some environments, that's okay
console.log(`(RPC unavailable: ${err instanceof Error ? err.message : String(err)})`);
}
});
// --- Summary ---
console.log(
`\n=== ${failed === 0 ? "ALL TESTS PASSED" : "SOME TESTS FAILED"} (${passed} passed, ${failed} failed) ===\n`,
);
process.exit(failed === 0 ? 0 : 1);
}
main().catch((err) => {
console.error("Fatal:", err);
process.exit(1);
});