@bfun-bot/cli 1.0.7 → 1.0.8

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/README.md CHANGED
@@ -17,7 +17,7 @@ bfunbot login
17
17
  # Create a token
18
18
  bfunbot token create --name "My Token" --symbol MYT
19
19
 
20
- # Check balances
20
+ # Check balances (with live USD values)
21
21
  bfunbot balances
22
22
 
23
23
  # Check fee earnings
@@ -35,15 +35,15 @@ bfunbot fees
35
35
  | `bfunbot token info <address>` | Get token details |
36
36
  | `bfunbot tokens created` | List tokens you've created |
37
37
  | `bfunbot status <jobId>` | Check job status |
38
- | `bfunbot balances` | Show wallet balances (BSC) |
39
- | `bfunbot quota` | Show daily API quota |
40
- | `bfunbot fees` | Check fee earnings summary |
41
- | `bfunbot fees --platform flap` | Per-platform fee breakdown |
38
+ | `bfunbot balances` | Show wallet balances (BSC) with live USD values |
39
+ | `bfunbot quota` | Show daily token creation quota |
40
+ | `bfunbot fees` | Check total fee earnings summary (with live USD) |
41
+ | `bfunbot fees breakdown` | Per-platform fee breakdown (flap + fourmeme) |
42
+ | `bfunbot fees --platform <p> --token <addr>` | Per-token fee detail |
42
43
  | `bfunbot llm models` | List available LLM models |
43
44
  | `bfunbot llm credits` | Check BFun.bot Credits balance |
44
45
  | `bfunbot llm reload` | Reload credits from trading wallet |
45
46
  | `bfunbot llm setup openclaw` | Configure OpenClaw integration |
46
- | `bfunbot skills` | List agent capabilities |
47
47
  | `bfunbot config get` | Show current config |
48
48
  | `bfunbot config set` | Set a config value |
49
49
  | `bfunbot about` | About BFunBot CLI |
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tests for bfunbot balances command.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { Command } from 'commander';
6
+ vi.mock('../../lib/api.js', () => ({
7
+ agent: {
8
+ walletBalance: vi.fn(),
9
+ },
10
+ handleApiError: vi.fn((err) => { throw err; }),
11
+ }));
12
+ import { agent } from '../../lib/api.js';
13
+ import { registerBalances } from '../../commands/balances.js';
14
+ const MOCK_BALANCE = {
15
+ evm_main: {
16
+ address: '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
17
+ balance_bnb: '0.412',
18
+ balance_usdt_bsc: '100.5',
19
+ bnb_error: null,
20
+ usdt_bsc_error: null,
21
+ },
22
+ evm_trading: {
23
+ address: '0xDeaD000000000000000042069420694206942069',
24
+ balance_bnb: '0.050',
25
+ balance_usdt_bsc: '0',
26
+ bnb_error: null,
27
+ usdt_bsc_error: null,
28
+ },
29
+ bnb_price_usd: 600,
30
+ };
31
+ function buildProgram() {
32
+ const program = new Command();
33
+ program.option('--json');
34
+ registerBalances(program);
35
+ return program;
36
+ }
37
+ describe('bfunbot balances', () => {
38
+ let output;
39
+ beforeEach(() => {
40
+ output = [];
41
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
42
+ output.push(args.map(String).join(' '));
43
+ });
44
+ vi.mocked(agent.walletBalance).mockResolvedValue(MOCK_BALANCE);
45
+ });
46
+ afterEach(() => vi.restoreAllMocks());
47
+ it('shows EVM Main wallet label', async () => {
48
+ const program = buildProgram();
49
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
50
+ expect(output.join('\n')).toContain('EVM Main');
51
+ });
52
+ it('shows EVM Trading wallet label', async () => {
53
+ const program = buildProgram();
54
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
55
+ expect(output.join('\n')).toContain('EVM Trading');
56
+ });
57
+ it('shows BNB balance', async () => {
58
+ const program = buildProgram();
59
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
60
+ expect(output.join('\n')).toContain('0.412');
61
+ });
62
+ it('shows USD equivalent alongside BNB', async () => {
63
+ const program = buildProgram();
64
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
65
+ expect(output.join('\n')).toContain('$');
66
+ });
67
+ it('shows USDT balance when non-zero', async () => {
68
+ const program = buildProgram();
69
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
70
+ expect(output.join('\n')).toContain('100.5');
71
+ });
72
+ it('shows error indicator when bnb_error is set', async () => {
73
+ vi.mocked(agent.walletBalance).mockResolvedValue({
74
+ ...MOCK_BALANCE,
75
+ evm_main: { ...MOCK_BALANCE.evm_main, bnb_error: 'rpc_unavailable' },
76
+ });
77
+ const stderrOutput = [];
78
+ vi.spyOn(console, 'error').mockImplementation((...args) => stderrOutput.push(args.map(String).join(' ')));
79
+ const program = buildProgram();
80
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
81
+ expect(output.join('\n')).toContain('error');
82
+ });
83
+ it('shows truncated address', async () => {
84
+ const program = buildProgram();
85
+ await program.parseAsync(['node', 'bfunbot', 'balances']);
86
+ expect(output.join('\n')).toContain('...');
87
+ });
88
+ it('outputs JSON when --json flag is passed', async () => {
89
+ const program = buildProgram();
90
+ await program.parseAsync(['node', 'bfunbot', '--json', 'balances']);
91
+ const jsonLine = output.find(l => l.trim().startsWith('{'));
92
+ expect(jsonLine).toBeDefined();
93
+ const parsed = JSON.parse(jsonLine);
94
+ expect(parsed.evm_main.balance_bnb).toBe('0.412');
95
+ expect(parsed.bnb_price_usd).toBe(600);
96
+ });
97
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Tests for bfunbot fees command.
3
+ *
4
+ * Mocks the agent API and asserts stdout output contains expected values.
5
+ * Does NOT assert exact formatting strings — those are covered in display.test.ts.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { Command } from 'commander';
9
+ // Mock the API module before importing the command
10
+ vi.mock('../../lib/api.js', () => ({
11
+ agent: {
12
+ feesSummary: vi.fn(),
13
+ feesEarnings: vi.fn(),
14
+ feesToken: vi.fn(),
15
+ },
16
+ handleApiError: vi.fn((err) => { throw err; }),
17
+ }));
18
+ import { agent } from '../../lib/api.js';
19
+ import { registerFees } from '../../commands/fees.js';
20
+ const MOCK_SUMMARY = {
21
+ bsc: { token_count: 3, total_earned_bnb: 0.00412 },
22
+ bnb_price_usd: 600,
23
+ };
24
+ const MOCK_EARNINGS = {
25
+ chain: 'bsc',
26
+ flap: { total_earned_bnb: 0.00300, earning_token_count: 2 },
27
+ fourmeme: { total_earned_bnb: 0.00112, earning_token_count: 1 },
28
+ };
29
+ const MOCK_TOKEN_BSC = {
30
+ token_address: '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
31
+ token_name: 'TestToken',
32
+ token_symbol: 'TEST',
33
+ platform: 'flap',
34
+ chain: 'bsc',
35
+ earned_bnb: 0.00300,
36
+ };
37
+ function buildProgram() {
38
+ const program = new Command();
39
+ program.option('--json');
40
+ registerFees(program);
41
+ return program;
42
+ }
43
+ describe('bfunbot fees (summary)', () => {
44
+ let output;
45
+ beforeEach(() => {
46
+ output = [];
47
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
48
+ output.push(args.map(String).join(' '));
49
+ });
50
+ vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
51
+ });
52
+ afterEach(() => vi.restoreAllMocks());
53
+ it('shows total earned BNB', async () => {
54
+ const program = buildProgram();
55
+ await program.parseAsync(['node', 'bfunbot', 'fees']);
56
+ const all = output.join('\n');
57
+ expect(all).toContain('BNB');
58
+ expect(all).toContain('0.00412');
59
+ });
60
+ it('shows USD value when bnb_price_usd is provided', async () => {
61
+ const program = buildProgram();
62
+ await program.parseAsync(['node', 'bfunbot', 'fees']);
63
+ const all = output.join('\n');
64
+ expect(all).toContain('$');
65
+ });
66
+ it('shows hint to run fees breakdown', async () => {
67
+ const program = buildProgram();
68
+ await program.parseAsync(['node', 'bfunbot', 'fees']);
69
+ expect(output.join('\n')).toContain('breakdown');
70
+ });
71
+ it('outputs JSON when --json flag is passed', async () => {
72
+ const program = buildProgram();
73
+ await program.parseAsync(['node', 'bfunbot', '--json', 'fees']);
74
+ const jsonLine = output.find(l => l.trim().startsWith('{'));
75
+ expect(jsonLine).toBeDefined();
76
+ const parsed = JSON.parse(jsonLine);
77
+ expect(parsed.bsc.total_earned_bnb).toBe(0.00412);
78
+ });
79
+ });
80
+ describe('bfunbot fees breakdown', () => {
81
+ let output;
82
+ beforeEach(() => {
83
+ output = [];
84
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
85
+ output.push(args.map(String).join(' '));
86
+ });
87
+ vi.mocked(agent.feesEarnings).mockResolvedValue(MOCK_EARNINGS);
88
+ vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
89
+ });
90
+ afterEach(() => vi.restoreAllMocks());
91
+ it('still works when feesSummary fails (graceful fallback — no USD)', async () => {
92
+ vi.mocked(agent.feesSummary).mockRejectedValue(new Error('summary unavailable'));
93
+ const program = buildProgram();
94
+ await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
95
+ const all = output.join('\n');
96
+ expect(all).toContain('flap');
97
+ expect(all).toContain('fourmeme');
98
+ });
99
+ it('does not call feesSummary when --json flag is used', async () => {
100
+ vi.mocked(agent.feesSummary).mockClear();
101
+ const program = buildProgram();
102
+ await program.parseAsync(['node', 'bfunbot', '--json', 'fees', 'breakdown']);
103
+ expect(agent.feesSummary).not.toHaveBeenCalled();
104
+ });
105
+ it('shows flap and fourmeme platforms', async () => {
106
+ const program = buildProgram();
107
+ await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
108
+ const all = output.join('\n');
109
+ expect(all).toContain('flap');
110
+ expect(all).toContain('fourmeme');
111
+ });
112
+ it('shows BNB earned for both platforms', async () => {
113
+ const program = buildProgram();
114
+ await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
115
+ const all = output.join('\n');
116
+ expect(all).toContain('0.003');
117
+ expect(all).toContain('0.00112');
118
+ });
119
+ it('shows USD column', async () => {
120
+ const program = buildProgram();
121
+ await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
122
+ expect(output.join('\n')).toContain('$');
123
+ });
124
+ it('shows token counts', async () => {
125
+ const program = buildProgram();
126
+ await program.parseAsync(['node', 'bfunbot', 'fees', 'breakdown']);
127
+ const all = output.join('\n');
128
+ expect(all).toContain('2');
129
+ expect(all).toContain('1');
130
+ });
131
+ });
132
+ describe('bfunbot fees --token --platform', () => {
133
+ let output;
134
+ beforeEach(() => {
135
+ output = [];
136
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
137
+ output.push(args.map(String).join(' '));
138
+ });
139
+ vi.mocked(agent.feesToken).mockResolvedValue(MOCK_TOKEN_BSC);
140
+ vi.mocked(agent.feesSummary).mockResolvedValue(MOCK_SUMMARY);
141
+ });
142
+ afterEach(() => vi.restoreAllMocks());
143
+ it('shows token name and symbol', async () => {
144
+ const program = buildProgram();
145
+ await program.parseAsync([
146
+ 'node', 'bfunbot', 'fees',
147
+ '--platform', 'flap',
148
+ '--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
149
+ ]);
150
+ const all = output.join('\n');
151
+ expect(all).toContain('TestToken');
152
+ expect(all).toContain('TEST');
153
+ });
154
+ it('shows earned BNB with USD', async () => {
155
+ const program = buildProgram();
156
+ await program.parseAsync([
157
+ 'node', 'bfunbot', 'fees',
158
+ '--platform', 'flap',
159
+ '--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
160
+ ]);
161
+ const all = output.join('\n');
162
+ expect(all).toContain('BNB');
163
+ expect(all).toContain('$');
164
+ });
165
+ it('errors if --token given without --platform', async () => {
166
+ const stderrOutput = [];
167
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
168
+ stderrOutput.push(args.map(String).join(' '));
169
+ });
170
+ vi.spyOn(process, 'exit').mockImplementation((() => {
171
+ throw new Error('process.exit(1)');
172
+ }));
173
+ const program = buildProgram();
174
+ await expect(program.parseAsync(['node', 'bfunbot', 'fees', '--token', '0xabc123'])).rejects.toThrow('process.exit(1)');
175
+ expect(stderrOutput[0]).toContain('--platform');
176
+ });
177
+ it('still works when feesSummary fails for per-token (graceful fallback)', async () => {
178
+ vi.mocked(agent.feesSummary).mockRejectedValue(new Error('summary unavailable'));
179
+ const program = buildProgram();
180
+ await program.parseAsync([
181
+ 'node', 'bfunbot', 'fees',
182
+ '--platform', 'flap',
183
+ '--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
184
+ ]);
185
+ const all = output.join('\n');
186
+ expect(all).toContain('TestToken');
187
+ expect(all).toContain('BNB');
188
+ });
189
+ it('does not call feesSummary for --token when --json flag is used', async () => {
190
+ vi.mocked(agent.feesSummary).mockClear();
191
+ const program = buildProgram();
192
+ await program.parseAsync([
193
+ 'node', 'bfunbot', '--json', 'fees',
194
+ '--platform', 'flap',
195
+ '--token', '0xB305ce8D75498E4D0c2963089C4894b3589F7777',
196
+ ]);
197
+ expect(agent.feesSummary).not.toHaveBeenCalled();
198
+ });
199
+ it('routes --platform alone to breakdown with deprecation hint', async () => {
200
+ vi.mocked(agent.feesEarnings).mockResolvedValue(MOCK_EARNINGS);
201
+ const program = buildProgram();
202
+ await program.parseAsync(['node', 'bfunbot', 'fees', '--platform', 'flap']);
203
+ const all = output.join('\n');
204
+ expect(all).toContain('flap');
205
+ expect(all).toContain('fourmeme');
206
+ expect(all).toContain('fees breakdown');
207
+ });
208
+ it('errors if invalid platform given', async () => {
209
+ const stderrOutput = [];
210
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
211
+ stderrOutput.push(args.map(String).join(' '));
212
+ });
213
+ vi.spyOn(process, 'exit').mockImplementation((() => {
214
+ throw new Error('process.exit(1)');
215
+ }));
216
+ const program = buildProgram();
217
+ await expect(program.parseAsync(['node', 'bfunbot', 'fees', '--platform', 'invalid', '--token', '0xabc'])).rejects.toThrow('process.exit(1)');
218
+ expect(stderrOutput[0]).toContain('Platform must be one of');
219
+ });
220
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tests for bfunbot quota command.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { Command } from 'commander';
6
+ vi.mock('../../lib/api.js', () => ({
7
+ agent: {
8
+ quota: vi.fn(),
9
+ },
10
+ handleApiError: vi.fn((err) => { throw err; }),
11
+ }));
12
+ import { agent } from '../../lib/api.js';
13
+ import { registerQuota } from '../../commands/quota.js';
14
+ const MOCK_QUOTA = {
15
+ chains: [
16
+ {
17
+ chain: 'bsc',
18
+ free_used_today: 2,
19
+ free_limit: 5,
20
+ sponsored_remaining: 3,
21
+ can_create_paid: true,
22
+ trading_wallet_balance: '0.050000 BNB',
23
+ trading_wallet_address: '0xDeaD000000000000000042069420694206942069',
24
+ },
25
+ ],
26
+ };
27
+ function buildProgram() {
28
+ const program = new Command();
29
+ program.option('--json');
30
+ registerQuota(program);
31
+ return program;
32
+ }
33
+ describe('bfunbot quota', () => {
34
+ let output;
35
+ beforeEach(() => {
36
+ output = [];
37
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
38
+ output.push(args.map(String).join(' '));
39
+ });
40
+ vi.mocked(agent.quota).mockResolvedValue(MOCK_QUOTA);
41
+ });
42
+ afterEach(() => vi.restoreAllMocks());
43
+ it('shows chain name', async () => {
44
+ const program = buildProgram();
45
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
46
+ expect(output.join('\n')).toContain('Bsc');
47
+ });
48
+ it('shows free used and limit', async () => {
49
+ const program = buildProgram();
50
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
51
+ const all = output.join('\n');
52
+ expect(all).toContain('2');
53
+ expect(all).toContain('5');
54
+ });
55
+ it('shows remaining quota count', async () => {
56
+ const program = buildProgram();
57
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
58
+ expect(output.join('\n')).toContain('3');
59
+ });
60
+ it('shows Yes for can_create_paid=true', async () => {
61
+ const program = buildProgram();
62
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
63
+ expect(output.join('\n')).toContain('Yes');
64
+ });
65
+ it('shows No only when both free remaining and can_create_paid are false/zero', async () => {
66
+ vi.mocked(agent.quota).mockResolvedValue({
67
+ chains: [{ ...MOCK_QUOTA.chains[0], can_create_paid: false, sponsored_remaining: 0 }],
68
+ });
69
+ const program = buildProgram();
70
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
71
+ expect(output.join('\n')).toContain('No');
72
+ });
73
+ it('shows Yes when free remaining > 0 even if can_create_paid is false', async () => {
74
+ vi.mocked(agent.quota).mockResolvedValue({
75
+ chains: [{ ...MOCK_QUOTA.chains[0], can_create_paid: false, sponsored_remaining: 9 }],
76
+ });
77
+ const program = buildProgram();
78
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
79
+ expect(output.join('\n')).toContain('Yes');
80
+ });
81
+ it('table columns align (ANSI-aware)', async () => {
82
+ const program = buildProgram();
83
+ await program.parseAsync(['node', 'bfunbot', 'quota']);
84
+ const strip = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
85
+ // Find the header row (contains all column names)
86
+ const headerIdx = output.findIndex(l => strip(l).includes('Chain') && strip(l).includes('Used Today'));
87
+ expect(headerIdx).toBeGreaterThan(-1);
88
+ const headerLen = strip(output[headerIdx]).length;
89
+ // Data rows immediately follow the separator line — check they match header width
90
+ const separatorIdx = output.findIndex((l, i) => i > headerIdx && strip(l).includes('─'));
91
+ const dataRows = output.slice(separatorIdx + 1).filter(l => l.trim() !== '');
92
+ for (const row of dataRows) {
93
+ expect(strip(row).length).toBe(headerLen);
94
+ }
95
+ });
96
+ it('outputs JSON when --json flag is passed', async () => {
97
+ const program = buildProgram();
98
+ await program.parseAsync(['node', 'bfunbot', '--json', 'quota']);
99
+ const jsonLine = output.find(l => l.trim().startsWith('{'));
100
+ expect(jsonLine).toBeDefined();
101
+ const parsed = JSON.parse(jsonLine);
102
+ expect(parsed.chains[0].chain).toBe('bsc');
103
+ });
104
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for src/lib/api.ts
3
+ *
4
+ * Covers: ApiError construction, handleApiError friendly messages,
5
+ * request() — correct headers, error mapping, network failure.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { ApiError, handleApiError } from '../../lib/api.js';
9
+ // ─── ApiError ────────────────────────────────────────────────────────────────
10
+ describe('ApiError', () => {
11
+ it('constructs with status code and message', () => {
12
+ const err = new ApiError(404, 'Not Found', { detail: 'Token not found' });
13
+ expect(err.statusCode).toBe(404);
14
+ expect(err.message).toBe('Token not found');
15
+ expect(err.name).toBe('ApiError');
16
+ });
17
+ it('falls back to statusText when no body detail', () => {
18
+ const err = new ApiError(500, 'Internal Server Error');
19
+ expect(err.message).toBe('Internal Server Error');
20
+ });
21
+ it('uses body.message if detail is absent', () => {
22
+ const err = new ApiError(400, 'Bad Request', { message: 'Bad input' });
23
+ expect(err.message).toBe('Bad input');
24
+ });
25
+ });
26
+ // ─── handleApiError friendly messages ────────────────────────────────────────
27
+ describe('handleApiError', () => {
28
+ let stderrOutput;
29
+ beforeEach(() => {
30
+ stderrOutput = [];
31
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
32
+ stderrOutput.push(args.map(String).join(' '));
33
+ });
34
+ vi.spyOn(process, 'exit').mockImplementation((() => { }));
35
+ });
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ });
39
+ it('shows login hint for 401', () => {
40
+ handleApiError(new ApiError(401, 'Unauthorized'));
41
+ expect(stderrOutput[0]).toContain('bfunbot login');
42
+ });
43
+ it('shows access denied for 403', () => {
44
+ handleApiError(new ApiError(403, 'Forbidden'));
45
+ expect(stderrOutput[0]).toContain('Access denied');
46
+ });
47
+ it('shows 403 detail string if provided', () => {
48
+ handleApiError(new ApiError(403, 'Forbidden', { detail: 'IP not allowed' }));
49
+ expect(stderrOutput[0]).toContain('IP not allowed');
50
+ });
51
+ it('shows rate limit message for 429', () => {
52
+ handleApiError(new ApiError(429, 'Too Many Requests'));
53
+ expect(stderrOutput[0]).toContain('Rate limited');
54
+ });
55
+ it('shows server error for 500+', () => {
56
+ handleApiError(new ApiError(503, 'Service Unavailable'));
57
+ expect(stderrOutput[0]).toContain('Server error');
58
+ });
59
+ it('joins FastAPI 422 validation errors', () => {
60
+ handleApiError(new ApiError(422, 'Unprocessable Entity', {
61
+ detail: [
62
+ { msg: 'field required', loc: ['body', 'name'], type: 'missing' },
63
+ { msg: 'invalid value', loc: ['body', 'symbol'], type: 'value_error' },
64
+ ],
65
+ }));
66
+ expect(stderrOutput[0]).toContain('field required');
67
+ expect(stderrOutput[0]).toContain('invalid value');
68
+ });
69
+ it('shows connection error for network TypeError', () => {
70
+ handleApiError(new TypeError('fetch failed', { cause: new Error('ECONNREFUSED') }));
71
+ expect(stderrOutput[0]).toContain('Could not reach');
72
+ });
73
+ it('calls process.exit(1)', () => {
74
+ handleApiError(new ApiError(401, 'Unauthorized'));
75
+ expect(process.exit).toHaveBeenCalledWith(1);
76
+ });
77
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Tests for src/lib/display.ts
3
+ *
4
+ * Covers: fmtBalance, fmtBnbWithUsd, fmtUsd, shortAddr, fmtDate, printTable alignment.
5
+ */
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import { fmtBalance, fmtBnbWithUsd, fmtUsd, shortAddr, fmtDate, printTable, } from '../../lib/display.js';
8
+ // Strip ANSI codes from output for readable assertions
9
+ const strip = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
10
+ // ─── fmtBalance ──────────────────────────────────────────────────────────────
11
+ describe('fmtBalance', () => {
12
+ it('formats a normal BNB value', () => {
13
+ expect(strip(fmtBalance('1.5', 'BNB'))).toBe('1.5 BNB');
14
+ });
15
+ it('trims trailing zeros', () => {
16
+ expect(strip(fmtBalance('1.500000', 'BNB'))).toBe('1.5 BNB');
17
+ });
18
+ it('formats a small value without scientific notation', () => {
19
+ const result = strip(fmtBalance('0.00000597', 'BNB'));
20
+ expect(result).not.toContain('e');
21
+ expect(result).toContain('0.00000597');
22
+ expect(result).toContain('BNB');
23
+ });
24
+ it('formats an extremely small value without scientific notation', () => {
25
+ const result = strip(fmtBalance('0.0000000012', 'BNB'));
26
+ expect(result).not.toContain('e');
27
+ expect(result).toContain('BNB');
28
+ });
29
+ it('returns dim zero for zero value', () => {
30
+ expect(strip(fmtBalance('0', 'BNB'))).toBe('0 BNB');
31
+ });
32
+ it('returns dim zero for null', () => {
33
+ expect(strip(fmtBalance(null, 'BNB'))).toBe('0 BNB');
34
+ });
35
+ it('returns dim zero for undefined', () => {
36
+ expect(strip(fmtBalance(undefined, 'BNB'))).toBe('0 BNB');
37
+ });
38
+ it('returns dim zero for "0.0"', () => {
39
+ expect(strip(fmtBalance('0.0', 'BNB'))).toBe('0 BNB');
40
+ });
41
+ it('formats USDT correctly', () => {
42
+ expect(strip(fmtBalance('100.5', 'USDT'))).toBe('100.5 USDT');
43
+ });
44
+ it('handles 6 decimal places without trailing zeros', () => {
45
+ expect(strip(fmtBalance('0.412000', 'BNB'))).toBe('0.412 BNB');
46
+ });
47
+ it('never shows 0 for a non-zero sub-1e-10 value', () => {
48
+ const result = strip(fmtBalance('0.00000000001', 'BNB'));
49
+ expect(result).not.toMatch(/^0 BNB/);
50
+ expect(result).not.toContain('e');
51
+ expect(result).toContain('BNB');
52
+ });
53
+ });
54
+ // ─── fmtBnbWithUsd ───────────────────────────────────────────────────────────
55
+ describe('fmtBnbWithUsd', () => {
56
+ it('appends USD value in parentheses', () => {
57
+ const result = strip(fmtBnbWithUsd('1.0', 600));
58
+ expect(result).toContain('1 BNB');
59
+ expect(result).toContain('~$600.00');
60
+ });
61
+ it('calculates USD correctly for fractional BNB', () => {
62
+ const result = strip(fmtBnbWithUsd('0.5', 600));
63
+ expect(result).toContain('~$300.00');
64
+ });
65
+ it('returns plain BNB when price is 0', () => {
66
+ const result = strip(fmtBnbWithUsd('1.0', 0));
67
+ expect(result).toContain('BNB');
68
+ expect(result).not.toContain('$');
69
+ });
70
+ it('returns plain BNB for null value', () => {
71
+ const result = strip(fmtBnbWithUsd(null, 600));
72
+ expect(result).not.toContain('$');
73
+ });
74
+ it('returns plain BNB for zero value', () => {
75
+ const result = strip(fmtBnbWithUsd('0', 600));
76
+ expect(result).not.toContain('$');
77
+ });
78
+ it('handles very small BNB with USD', () => {
79
+ const result = strip(fmtBnbWithUsd('0.00001', 600));
80
+ expect(result).not.toContain('e');
81
+ expect(result).toContain('BNB');
82
+ expect(result).toContain('~$');
83
+ });
84
+ });
85
+ // ─── fmtUsd ──────────────────────────────────────────────────────────────────
86
+ describe('fmtUsd', () => {
87
+ it('formats values >= $1 to 2 decimal places', () => {
88
+ expect(strip(fmtUsd('12.5'))).toBe('$12.50');
89
+ });
90
+ it('formats zero with em dash', () => {
91
+ expect(strip(fmtUsd('0'))).toBe('—');
92
+ });
93
+ it('formats null with em dash', () => {
94
+ expect(strip(fmtUsd(null))).toBe('—');
95
+ });
96
+ it('formats undefined with em dash', () => {
97
+ expect(strip(fmtUsd(undefined))).toBe('—');
98
+ });
99
+ it('formats small values with enough significant digits', () => {
100
+ const result = strip(fmtUsd('0.000123'));
101
+ expect(result).toContain('$');
102
+ expect(result).toContain('0.000123');
103
+ });
104
+ it('formats large USD values correctly', () => {
105
+ expect(strip(fmtUsd('1234.567'))).toBe('$1234.57');
106
+ });
107
+ });
108
+ // ─── shortAddr ───────────────────────────────────────────────────────────────
109
+ describe('shortAddr', () => {
110
+ it('truncates a long address', () => {
111
+ const addr = '0xB305ce8D75498E4D0c2963089C4894b3589F7777';
112
+ const result = strip(shortAddr(addr));
113
+ expect(result).toContain('0xB305ce');
114
+ expect(result).toContain('...');
115
+ expect(result).toContain('F7777');
116
+ expect(result.length).toBeLessThan(addr.length);
117
+ });
118
+ it('returns em dash for null', () => {
119
+ expect(strip(shortAddr(null))).toBe('—');
120
+ });
121
+ it('returns em dash for undefined', () => {
122
+ expect(strip(shortAddr(undefined))).toBe('—');
123
+ });
124
+ it('returns short address as-is', () => {
125
+ expect(strip(shortAddr('0x1234'))).toBe('0x1234');
126
+ });
127
+ });
128
+ // ─── fmtDate ─────────────────────────────────────────────────────────────────
129
+ describe('fmtDate', () => {
130
+ it('formats a valid ISO date string', () => {
131
+ const result = strip(fmtDate('2024-01-15T10:30:00Z'));
132
+ expect(result).toContain('2024');
133
+ expect(result).toContain('Jan');
134
+ expect(result).toContain('15');
135
+ });
136
+ it('returns em dash for null', () => {
137
+ expect(strip(fmtDate(null))).toBe('—');
138
+ });
139
+ it('returns em dash for undefined', () => {
140
+ expect(strip(fmtDate(undefined))).toBe('—');
141
+ });
142
+ });
143
+ // ─── printTable alignment ────────────────────────────────────────────────────
144
+ describe('printTable', () => {
145
+ let output;
146
+ beforeEach(() => {
147
+ output = [];
148
+ vi.spyOn(console, 'log').mockImplementation((...args) => {
149
+ output.push(args.map(String).join(' '));
150
+ });
151
+ });
152
+ afterEach(() => {
153
+ vi.restoreAllMocks();
154
+ });
155
+ it('produces consistent column widths with plain text', () => {
156
+ printTable(['Name', 'Value', 'Status'], [
157
+ ['Alice', '100', 'active'],
158
+ ['Bob', '9999', 'inactive'],
159
+ ]);
160
+ const rows = output.filter(l => l.trim() && !l.includes('─'));
161
+ // All rows should have the same length (same column widths)
162
+ const header = strip(rows[0]);
163
+ const row1 = strip(rows[1]);
164
+ const row2 = strip(rows[2]);
165
+ expect(row1.length).toBe(header.length);
166
+ expect(row2.length).toBe(header.length);
167
+ });
168
+ it('correctly aligns columns when cells contain chalk colors (ANSI codes)', async () => {
169
+ const { default: chalk } = await import('chalk');
170
+ printTable(['Chain', 'Used Today', 'Can Create'], [
171
+ [chalk.bold('BSC'), '5', chalk.green('Yes')],
172
+ [chalk.bold('ETH'), '10', chalk.dim('No')],
173
+ ]);
174
+ const rows = output.filter(l => l.trim() && !l.includes('─'));
175
+ // Visible (stripped) lengths of all rows must be equal
176
+ const lengths = rows.map(r => strip(r).length);
177
+ expect(lengths.every(l => l === lengths[0])).toBe(true);
178
+ });
179
+ it('correctly handles CJK wide characters in column width calculation', async () => {
180
+ printTable(['Name', 'Symbol'], [
181
+ ['普通Token', 'ABC'], // CJK chars — each is 2 columns wide
182
+ ['NormalName', 'XYZ'],
183
+ ]);
184
+ // Find separator line to locate data rows
185
+ const sepIdx = output.findIndex(l => strip(l).includes('─'));
186
+ const dataRows = output.slice(sepIdx + 1).filter(l => l.trim() !== '');
187
+ // Both data rows must have the same visible terminal width
188
+ const visW = (s) => {
189
+ let w = 0;
190
+ for (const ch of strip(s).trimEnd()) {
191
+ const cp = ch.codePointAt(0) ?? 0;
192
+ w += (cp >= 0x4E00 && cp <= 0x9FFF) ? 2 : 1;
193
+ }
194
+ return w;
195
+ };
196
+ const lengths = dataRows.map(r => visW(r));
197
+ expect(lengths).toHaveLength(2);
198
+ expect(lengths[0]).toBe(lengths[1]);
199
+ });
200
+ it('handles empty rows gracefully', () => {
201
+ expect(() => printTable(['Col A', 'Col B'], [])).not.toThrow();
202
+ });
203
+ it('renders headers and separator', () => {
204
+ printTable(['Symbol', 'Name'], [['BTC', 'Bitcoin']]);
205
+ const allOutput = output.join('\n');
206
+ expect(allOutput).toContain('Symbol');
207
+ expect(allOutput).toContain('Name');
208
+ expect(allOutput).toContain('─');
209
+ });
210
+ });
@@ -1,24 +1,23 @@
1
1
  import chalk from 'chalk';
2
2
  import { agent, handleApiError } from '../lib/api.js';
3
- import { fmtBalance, shortAddr } from '../lib/display.js';
4
- function printWallet(label, slot) {
3
+ import { fmtBalance, fmtBnbWithUsd, shortAddr } from '../lib/display.js';
4
+ function printWallet(label, slot, bnbPriceUsd) {
5
5
  if (!slot.address) {
6
6
  console.log(` ${chalk.dim(label + ':')} ${chalk.dim('Not configured')}`);
7
7
  return;
8
8
  }
9
9
  console.log(` ${chalk.bold(label)} ${chalk.dim(shortAddr(slot.address))}`);
10
- const balances = [
11
- ['BNB (BSC)', slot.balance_bnb, slot.bnb_error],
12
- ['USDT (BSC)', slot.balance_usdt_bsc, slot.usdt_bsc_error],
13
- ];
14
- for (const [name, value, error] of balances) {
15
- if (error) {
16
- console.log(` ${name.padEnd(14)} ${chalk.red('error')}`);
17
- }
18
- else if (name === 'BNB (BSC)' || (value && parseFloat(value) > 0)) {
19
- const symbol = name.split(' ')[0];
20
- console.log(` ${name.padEnd(14)} ${fmtBalance(value, symbol)}`);
21
- }
10
+ if (slot.bnb_error) {
11
+ console.log(` ${'BNB (BSC)'.padEnd(14)} ${chalk.red('error')}`);
12
+ }
13
+ else {
14
+ console.log(` ${'BNB (BSC)'.padEnd(14)} ${fmtBnbWithUsd(slot.balance_bnb, bnbPriceUsd)}`);
15
+ }
16
+ if (slot.usdt_bsc_error) {
17
+ console.log(` ${'USDT (BSC)'.padEnd(14)} ${chalk.red('error')}`);
18
+ }
19
+ else if (slot.balance_usdt_bsc && parseFloat(slot.balance_usdt_bsc) > 0) {
20
+ console.log(` ${'USDT (BSC)'.padEnd(14)} ${fmtBalance(slot.balance_usdt_bsc, 'USDT')}`);
22
21
  }
23
22
  console.log();
24
23
  }
@@ -38,8 +37,9 @@ export function registerBalances(program) {
38
37
  console.log(chalk.bold.cyan('Wallet Balances'));
39
38
  console.log(chalk.dim('─'.repeat(40)));
40
39
  console.log();
41
- printWallet('EVM Main', res.evm_main);
42
- printWallet('EVM Trading', res.evm_trading);
40
+ const bnbPrice = res.bnb_price_usd ?? 0;
41
+ printWallet('EVM Main', res.evm_main, bnbPrice);
42
+ printWallet('EVM Trading', res.evm_trading, bnbPrice);
43
43
  }
44
44
  catch (err) {
45
45
  handleApiError(err);
@@ -1,7 +1,7 @@
1
1
  /**
2
- * bfunbot fees — summary
3
- * bfunbot fees --platform flap — per-platform breakdown
4
- * bfunbot fees --token <addr> single token fee detail
2
+ * bfunbot fees — summary (total earned)
3
+ * bfunbot fees breakdown — per-platform table
4
+ * bfunbot fees --token <addr> --platform <plat> per-token detail
5
5
  */
6
6
  import { Command } from 'commander';
7
7
  export declare function registerFees(program: Command): void;
@@ -1,28 +1,40 @@
1
1
  import chalk from 'chalk';
2
2
  import { agent, handleApiError } from '../lib/api.js';
3
- import { fmtBalance, printTable } from '../lib/display.js';
3
+ import { fmtBalance, fmtBnbWithUsd, printTable } from '../lib/display.js';
4
4
  const VALID_PLATFORMS = ['flap', 'fourmeme'];
5
5
  // ─── Display Helpers ─────────────────────────────────────
6
6
  function printSummary(res) {
7
+ const bnbPrice = res.bnb_price_usd ?? 0;
7
8
  console.log();
8
9
  console.log(chalk.bold.cyan('Fee Earnings Summary'));
9
10
  console.log(chalk.dim('─'.repeat(44)));
10
11
  console.log();
11
- console.log(` ${chalk.bold('BSC (BNB Chain)')} ${chalk.dim(res.bsc.token_count + ' tokens')}`);
12
- console.log(` ${'Total Earned'.padEnd(22)} ${fmtBalance(res.bsc.total_earned_bnb.toString(), 'BNB')}`);
12
+ console.log(` ${chalk.bold('BSC (BNB Chain)')}`);
13
+ console.log(` ${'Total Earned'.padEnd(22)} ${fmtBnbWithUsd(res.bsc.total_earned_bnb.toString(), bnbPrice)}`);
14
+ console.log();
15
+ console.log(chalk.dim(` For per-platform breakdown: bfunbot fees breakdown`));
13
16
  console.log();
14
17
  }
15
- function printEarnings(res) {
18
+ function printBreakdown(res, bnbPriceUsd) {
16
19
  console.log();
17
- console.log(chalk.bold.cyan(`Fee Earnings · BSC`));
20
+ console.log(chalk.bold.cyan('Fee Earnings · Platform Breakdown'));
18
21
  console.log(chalk.dim('─'.repeat(44)));
19
- console.log();
20
- printTable(['Platform', 'Earned', 'Tokens'], [
21
- ['flap', fmtBalance(res.flap.total_earned_bnb.toString(), 'BNB'), res.flap.earning_token_count.toString()],
22
- ['fourmeme', fmtBalance(res.fourmeme.total_earned_bnb.toString(), 'BNB'), res.fourmeme.earning_token_count.toString()],
22
+ printTable(['Platform', 'Earned', 'USD Value', 'Tokens'], [
23
+ [
24
+ 'flap',
25
+ fmtBalance(res.flap.total_earned_bnb.toString(), 'BNB'),
26
+ bnbPriceUsd ? chalk.dim(`~$${(res.flap.total_earned_bnb * bnbPriceUsd).toFixed(2)}`) : chalk.dim('—'),
27
+ res.flap.earning_token_count.toString(),
28
+ ],
29
+ [
30
+ 'fourmeme',
31
+ fmtBalance(res.fourmeme.total_earned_bnb.toString(), 'BNB'),
32
+ bnbPriceUsd ? chalk.dim(`~$${(res.fourmeme.total_earned_bnb * bnbPriceUsd).toFixed(2)}`) : chalk.dim('—'),
33
+ res.fourmeme.earning_token_count.toString(),
34
+ ],
23
35
  ]);
24
36
  }
25
- function printToken(res) {
37
+ function printToken(res, bnbPriceUsd) {
26
38
  if ('supported' in res && res.supported === false) {
27
39
  console.log();
28
40
  console.log(chalk.yellow('⚠') + ' ' + res.message);
@@ -37,16 +49,16 @@ function printToken(res) {
37
49
  console.log(` ${'Token'.padEnd(14)} ${bsc.token_name} (${bsc.token_symbol})`);
38
50
  console.log(` ${'Address'.padEnd(14)} ${chalk.dim(bsc.token_address)}`);
39
51
  console.log(` ${'Platform'.padEnd(14)} ${bsc.platform}`);
40
- console.log(` ${'Earned'.padEnd(14)} ${fmtBalance(bsc.earned_bnb.toString(), 'BNB')}`);
52
+ console.log(` ${'Earned'.padEnd(14)} ${fmtBnbWithUsd(bsc.earned_bnb.toString(), bnbPriceUsd)}`);
41
53
  console.log();
42
54
  }
43
55
  // ─── Command Registration ─────────────────────────────────
44
56
  export function registerFees(program) {
45
- program
57
+ const feesCmd = program
46
58
  .command('fees')
47
- .description('Check fee earnings (summary, per-platform, or per-token)')
48
- .option('--platform <platform>', 'Platform: flap or fourmeme')
49
- .option('--token <address>', 'Token contract address')
59
+ .description('Check fee earnings (summary, breakdown, or per-token)')
60
+ .option('--platform <platform>', 'Platform for per-token lookup: flap or fourmeme')
61
+ .option('--token <address>', 'Token contract address (requires --platform)')
50
62
  .action(async (opts, cmd) => {
51
63
  const isJson = cmd.optsWithGlobals().json;
52
64
  try {
@@ -61,27 +73,35 @@ export function registerFees(program) {
61
73
  console.error(chalk.red('Error:') + ` Platform must be one of: ${VALID_PLATFORMS.join(', ')}`);
62
74
  process.exit(1);
63
75
  }
64
- const res = await agent.feesToken('bsc', platform, opts.token);
76
+ const tokenRes = await agent.feesToken('bsc', platform, opts.token);
65
77
  if (isJson) {
66
- console.log(JSON.stringify(res, null, 2));
78
+ console.log(JSON.stringify(tokenRes, null, 2));
67
79
  return;
68
80
  }
69
- printToken(res);
81
+ // Fetch price separately — graceful fallback if summary is unavailable
82
+ let bnbPrice = 0;
83
+ try {
84
+ bnbPrice = (await agent.feesSummary()).bnb_price_usd ?? 0;
85
+ }
86
+ catch { /* non-fatal */ }
87
+ printToken(tokenRes, bnbPrice);
70
88
  return;
71
89
  }
72
- // Per-platform breakdown
90
+ // --platform alone (legacy): route to breakdown with deprecation hint
73
91
  if (opts.platform) {
74
- if (!VALID_PLATFORMS.includes(opts.platform)) {
75
- console.error(chalk.red('Error:') + ` Platform must be one of: ${VALID_PLATFORMS.join(', ')}`);
76
- process.exit(1);
77
- }
78
- // Show full breakdown (both platforms) — the platform flag is for future filtering
79
- const res = await agent.feesEarnings('bsc');
92
+ if (!isJson)
93
+ console.log(chalk.dim(' Tip: use `bfunbot fees breakdown` for per-platform earnings'));
94
+ const earningsRes = await agent.feesEarnings('bsc');
80
95
  if (isJson) {
81
- console.log(JSON.stringify(res, null, 2));
96
+ console.log(JSON.stringify(earningsRes, null, 2));
82
97
  return;
83
98
  }
84
- printEarnings(res);
99
+ let bnbPrice = 0;
100
+ try {
101
+ bnbPrice = (await agent.feesSummary()).bnb_price_usd ?? 0;
102
+ }
103
+ catch { /* non-fatal */ }
104
+ printBreakdown(earningsRes, bnbPrice);
85
105
  return;
86
106
  }
87
107
  // Default: summary
@@ -96,4 +116,28 @@ export function registerFees(program) {
96
116
  handleApiError(err);
97
117
  }
98
118
  });
119
+ // ── fees breakdown ────────────────────────────────────
120
+ feesCmd
121
+ .command('breakdown')
122
+ .description('Per-platform fee earnings (flap + fourmeme)')
123
+ .action(async (_, cmd) => {
124
+ const isJson = cmd.optsWithGlobals().json;
125
+ try {
126
+ const earningsRes = await agent.feesEarnings('bsc');
127
+ if (isJson) {
128
+ console.log(JSON.stringify(earningsRes, null, 2));
129
+ return;
130
+ }
131
+ // Price fetch is non-fatal — breakdown still works without USD values
132
+ let bnbPrice = 0;
133
+ try {
134
+ bnbPrice = (await agent.feesSummary()).bnb_price_usd ?? 0;
135
+ }
136
+ catch { /* non-fatal */ }
137
+ printBreakdown(earningsRes, bnbPrice);
138
+ }
139
+ catch (err) {
140
+ handleApiError(err);
141
+ }
142
+ });
99
143
  }
@@ -49,12 +49,11 @@ export function registerLlm(program) {
49
49
  return;
50
50
  }
51
51
  printCard('BFun.bot Credits', [
52
- ['Balance', `$${res.balance_usd}`],
52
+ ['Balance', chalk.bold.white(`$${res.balance_usd}`)],
53
53
  ]);
54
54
  // Show agent reload config
55
55
  if (res.agent_reload) {
56
56
  const r = res.agent_reload;
57
- console.log();
58
57
  if (r.enabled) {
59
58
  console.log(` ${chalk.dim('Agent Reload:')} ${chalk.green('ON')}`);
60
59
  console.log(` ${chalk.dim('Amount:')} $${r.amount_usd.toFixed(2)}`);
@@ -13,13 +13,16 @@ export function registerQuota(program) {
13
13
  console.log(JSON.stringify(res, null, 2));
14
14
  return;
15
15
  }
16
- printTable(['Chain', 'Free Used', 'Free Limit', 'Sponsored', 'Can Create'], res.chains.map((c) => [
17
- chalk.bold(c.chain.charAt(0).toUpperCase() + c.chain.slice(1)),
18
- `${c.free_used_today}`,
19
- `${c.free_limit}`,
20
- `${c.sponsored_remaining}`,
21
- c.can_create_paid ? chalk.green('Yes') : chalk.dim('No'),
22
- ]));
16
+ printTable(['Chain', 'Used Today', 'Daily Limit', 'Remaining', 'Can Create'], res.chains.map((c) => {
17
+ const canCreate = c.sponsored_remaining > 0 || c.can_create_paid;
18
+ return [
19
+ chalk.bold(c.chain.charAt(0).toUpperCase() + c.chain.slice(1)),
20
+ `${c.free_used_today}`,
21
+ `${c.free_limit}`,
22
+ `${c.sponsored_remaining}`,
23
+ canCreate ? chalk.green('Yes') : chalk.dim('No'),
24
+ ];
25
+ }));
23
26
  }
24
27
  catch (err) {
25
28
  handleApiError(err);
@@ -4,8 +4,8 @@ import { agent, handleApiError } from '../lib/api.js';
4
4
  import { printCard, fmtUsd, fmtDate } from '../lib/display.js';
5
5
  const POLL_INTERVAL_MS = 2000;
6
6
  const POLL_TIMEOUT_MS = 120_000;
7
- const EXPLORER_URLS = {
8
- bsc: 'https://bscscan.com/token/',
7
+ const TOKEN_VIEW_URLS = {
8
+ bsc: 'https://bfun.bot/tokens/',
9
9
  };
10
10
  export function registerToken(program) {
11
11
  const tokenCmd = program
@@ -97,7 +97,7 @@ export function registerToken(program) {
97
97
  printCard('Token Deployed', [
98
98
  ['Address', job.token_address],
99
99
  ['Chain', 'BSC'],
100
- ['View', `${EXPLORER_URLS.bsc}${job.token_address}`],
100
+ ['View', `${TOKEN_VIEW_URLS.bsc}${job.token_address}`],
101
101
  ]);
102
102
  return;
103
103
  }
@@ -129,7 +129,7 @@ export function registerToken(program) {
129
129
  console.log(JSON.stringify(res, null, 2));
130
130
  return;
131
131
  }
132
- const explorerBase = EXPLORER_URLS[res.chain] || '';
132
+ const viewBase = TOKEN_VIEW_URLS[res.chain] || '';
133
133
  printCard(`${res.symbol} — ${res.name}`, [
134
134
  ['Address', res.token_address],
135
135
  ['Chain', res.chain],
@@ -140,7 +140,7 @@ export function registerToken(program) {
140
140
  ['24h Volume', fmtUsd(res.volume_24h_usd)],
141
141
  ['Creator Reward', fmtUsd(res.creator_reward_usd)],
142
142
  ['Created', fmtDate(res.created_at)],
143
- ['Explorer', explorerBase ? `${explorerBase}${res.token_address}` : undefined],
143
+ ['View', viewBase ? `${viewBase}${res.token_address}` : undefined],
144
144
  ]);
145
145
  }
146
146
  catch (err) {
package/dist/index.js CHANGED
@@ -17,7 +17,6 @@ import { registerToken } from './commands/token.js';
17
17
  import { registerStatus } from './commands/status.js';
18
18
  import { registerBalances } from './commands/balances.js';
19
19
  import { registerQuota } from './commands/quota.js';
20
- import { registerSkills } from './commands/skills.js';
21
20
  import { registerLlm } from './commands/llm.js';
22
21
  import { registerConfig } from './commands/config.js';
23
22
  import { registerAbout } from './commands/about.js';
@@ -78,7 +77,6 @@ registerToken(program);
78
77
  registerStatus(program);
79
78
  registerBalances(program);
80
79
  registerQuota(program);
81
- registerSkills(program);
82
80
  registerLlm(program);
83
81
  registerConfig(program);
84
82
  registerAbout(program);
@@ -4,6 +4,7 @@
4
4
  export declare function printCard(title: string, fields: [string, string | undefined][]): void;
5
5
  /**
6
6
  * Print a simple table with headers.
7
+ * Uses ANSI-stripped lengths for column width calculation so chalk colors don't break alignment.
7
8
  */
8
9
  export declare function printTable(headers: string[], rows: string[][]): void;
9
10
  /**
@@ -14,6 +15,11 @@ export declare function shortAddr(addr: string | undefined | null, chars?: numbe
14
15
  * Format a balance value.
15
16
  */
16
17
  export declare function fmtBalance(value: string | undefined | null, symbol: string): string;
18
+ /**
19
+ * Format a BNB value with USD equivalent in parentheses.
20
+ * e.g. "0.00412 BNB (~$2.47)"
21
+ */
22
+ export declare function fmtBnbWithUsd(value: string | undefined | null, bnbPriceUsd: number): string;
17
23
  /**
18
24
  * Format a USD value.
19
25
  */
@@ -2,6 +2,7 @@
2
2
  * Terminal display helpers — tables, cards, formatting.
3
3
  */
4
4
  import chalk from 'chalk';
5
+ import stringWidth from 'string-width';
5
6
  /**
6
7
  * Print a key-value card.
7
8
  */
@@ -16,23 +17,38 @@ export function printCard(title, fields) {
16
17
  }
17
18
  console.log();
18
19
  }
20
+ /**
21
+ * Get the visible terminal width of a string.
22
+ * Uses string-width which correctly handles ANSI codes, CJK wide chars,
23
+ * ZWJ emoji sequences, variation selectors, and all Unicode edge cases.
24
+ */
25
+ function visibleWidth(str) {
26
+ return stringWidth(str);
27
+ }
19
28
  /**
20
29
  * Print a simple table with headers.
30
+ * Uses ANSI-stripped lengths for column width calculation so chalk colors don't break alignment.
21
31
  */
22
32
  export function printTable(headers, rows) {
23
- // Calculate column widths
33
+ // Calculate column widths using visible terminal width (handles ANSI + CJK wide chars)
24
34
  const widths = headers.map((h, i) => {
25
- const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] || '').length), 0);
26
- return Math.max(h.length, maxRow);
35
+ const maxRow = rows.reduce((max, row) => Math.max(max, visibleWidth(row[i] || '')), 0);
36
+ return Math.max(visibleWidth(h), maxRow);
27
37
  });
28
38
  // Header
29
- const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
39
+ const headerLine = headers.map((h, i) => {
40
+ const padding = widths[i] - visibleWidth(h);
41
+ return h + ' '.repeat(Math.max(0, padding));
42
+ }).join(' ');
30
43
  console.log();
31
44
  console.log(chalk.bold(headerLine));
32
45
  console.log(chalk.dim(widths.map(w => '─'.repeat(w)).join(' ')));
33
- // Rows
46
+ // Rows — pad by visible terminal width, not raw string length
34
47
  for (const row of rows) {
35
- const line = row.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ');
48
+ const line = row.map((cell, i) => {
49
+ const padding = widths[i] - visibleWidth(cell || '');
50
+ return (cell || '') + ' '.repeat(Math.max(0, padding));
51
+ }).join(' ');
36
52
  console.log(line);
37
53
  }
38
54
  console.log();
@@ -59,9 +75,45 @@ export function fmtBalance(value, symbol) {
59
75
  return chalk.dim(`0 ${symbol}`);
60
76
  if (num === 0)
61
77
  return chalk.dim(`0 ${symbol}`);
62
- const formatted = num < 0.0001 ? num.toExponential(2) : num.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
78
+ // Use enough decimal places to always show significant digits for very small values.
79
+ // toFixed(10) rounds below 5e-11 to zero, so fall back to toPrecision for anything smaller.
80
+ let formatted;
81
+ if (num >= 0.0001) {
82
+ formatted = num.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
83
+ }
84
+ else if (num >= 5e-11) {
85
+ formatted = num.toFixed(10).replace(/0+$/, '').replace(/\.$/, '');
86
+ }
87
+ else {
88
+ // Extremely small: toPrecision gives enough sig figs, then convert from sci notation
89
+ formatted = parseFloat(num.toPrecision(4)).toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
90
+ }
63
91
  return `${formatted} ${symbol}`;
64
92
  }
93
+ /**
94
+ * Format a BNB value with USD equivalent in parentheses.
95
+ * e.g. "0.00412 BNB (~$2.47)"
96
+ */
97
+ export function fmtBnbWithUsd(value, bnbPriceUsd) {
98
+ const bnbStr = fmtBalance(value, 'BNB');
99
+ if (!value || value === '0' || value === '0.0' || !bnbPriceUsd)
100
+ return bnbStr;
101
+ const num = parseFloat(value);
102
+ if (isNaN(num) || num === 0)
103
+ return bnbStr;
104
+ const usd = num * bnbPriceUsd;
105
+ return `${bnbStr} ${chalk.dim('(~' + fmtUsdInline(usd) + ')')}`;
106
+ }
107
+ function fmtUsdInline(usd) {
108
+ if (usd >= 1)
109
+ return `$${usd.toFixed(2)}`;
110
+ if (usd === 0)
111
+ return '$0';
112
+ const str = usd.toFixed(10);
113
+ const match = str.match(/^0\.(0*)/);
114
+ const leadingZeros = match ? match[1].length : 0;
115
+ return `$${usd.toFixed(leadingZeros + 4)}`;
116
+ }
65
117
  /**
66
118
  * Format a USD value.
67
119
  */
package/dist/types.d.ts CHANGED
@@ -85,6 +85,7 @@ export interface WalletSlot {
85
85
  export interface WalletBalanceResponse {
86
86
  evm_main: WalletSlot;
87
87
  evm_trading: WalletSlot;
88
+ bnb_price_usd?: number;
88
89
  }
89
90
  export interface CreditBalanceResponse {
90
91
  balance_usd: string;
@@ -144,6 +145,7 @@ export interface FeeSummaryBsc {
144
145
  }
145
146
  export interface FeeSummaryResponse {
146
147
  bsc: FeeSummaryBsc;
148
+ bnb_price_usd?: number;
147
149
  }
148
150
  export interface FeeEarningsBsc {
149
151
  chain: 'bsc';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bfun-bot/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "BFunBot CLI — deploy tokens, check balances, and manage your AI agent from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,9 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "dev": "tsc --watch",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:coverage": "vitest run --coverage",
19
22
  "prepublishOnly": "npm run build"
20
23
  },
21
24
  "engines": {
@@ -35,10 +38,13 @@
35
38
  "dependencies": {
36
39
  "chalk": "^5.3.0",
37
40
  "commander": "^12.1.0",
38
- "ora": "^8.1.1"
41
+ "ora": "^8.1.1",
42
+ "string-width": "^8.2.0"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/node": "^22.0.0",
42
- "typescript": "^5.5.4"
46
+ "@vitest/coverage-v8": "^4.1.2",
47
+ "typescript": "^5.5.4",
48
+ "vitest": "^4.1.2"
43
49
  }
44
50
  }