@paylobster/cli 4.2.0 → 4.3.0

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.
@@ -0,0 +1,907 @@
1
+ import { Command } from 'commander';
2
+ import { type Address, formatUnits, parseUnits, decodeEventLog } from 'viem';
3
+ import { base } from 'viem/chains';
4
+ import { getWalletAddress, loadWallet } from '../lib/wallet';
5
+ import { getPublicClient } from '../lib/contracts';
6
+ import { success, error, info, withSpinner, outputJSON, confirm, createTable, formatAddress } from '../lib/display';
7
+ import chalk from 'chalk';
8
+ import type { OutputOptions } from '../lib/types';
9
+
10
+ // Treasury contracts
11
+ const TREASURY_FACTORY_ADDRESS = '0x171a685f28546a0ebb13059184db1f808b915066' as Address;
12
+ const BASE_CHAIN_ID = 8453;
13
+
14
+ // Role enum mapping
15
+ enum Role {
16
+ NONE = 0,
17
+ VIEWER = 1,
18
+ OPERATOR = 2,
19
+ ADMIN = 3,
20
+ }
21
+
22
+ const ROLE_NAMES: Record<number, string> = {
23
+ 0: 'None',
24
+ 1: 'Viewer',
25
+ 2: 'Operator',
26
+ 3: 'Admin',
27
+ };
28
+
29
+ // ABIs
30
+ const TREASURY_FACTORY_ABI = [
31
+ {
32
+ type: 'function',
33
+ name: 'createTreasury',
34
+ inputs: [{ name: '_name', type: 'string' }],
35
+ outputs: [{ name: '', type: 'address' }],
36
+ stateMutability: 'nonpayable',
37
+ },
38
+ {
39
+ type: 'function',
40
+ name: 'treasuries',
41
+ inputs: [{ name: '', type: 'address' }],
42
+ outputs: [{ name: '', type: 'address' }],
43
+ stateMutability: 'view',
44
+ },
45
+ {
46
+ type: 'event',
47
+ name: 'TreasuryCreated',
48
+ inputs: [
49
+ { name: 'owner', type: 'address', indexed: true },
50
+ { name: 'treasury', type: 'address', indexed: true },
51
+ { name: 'name', type: 'string', indexed: false },
52
+ ],
53
+ },
54
+ ] as const;
55
+
56
+ const TREASURY_ABI = [
57
+ {
58
+ type: 'function',
59
+ name: 'getSummary',
60
+ inputs: [],
61
+ outputs: [
62
+ { name: '_name', type: 'string' },
63
+ { name: '_owner', type: 'address' },
64
+ { name: 'ethBalance', type: 'uint256' },
65
+ { name: 'tokenCount', type: 'uint256' },
66
+ { name: 'memberCount', type: 'uint256' },
67
+ { name: '_totalDeposited', type: 'uint256' },
68
+ { name: '_totalWithdrawn', type: 'uint256' },
69
+ { name: '_reserveLockBps', type: 'uint256' },
70
+ { name: '_createdAt', type: 'uint256' },
71
+ ],
72
+ stateMutability: 'view',
73
+ },
74
+ {
75
+ type: 'function',
76
+ name: 'getBalances',
77
+ inputs: [],
78
+ outputs: [
79
+ {
80
+ name: '',
81
+ type: 'tuple[]',
82
+ components: [
83
+ { name: 'token', type: 'address' },
84
+ { name: 'balance', type: 'uint256' },
85
+ ],
86
+ },
87
+ ],
88
+ stateMutability: 'view',
89
+ },
90
+ {
91
+ type: 'function',
92
+ name: 'deposit',
93
+ inputs: [
94
+ { name: 'token', type: 'address' },
95
+ { name: 'amount', type: 'uint256' },
96
+ ],
97
+ outputs: [],
98
+ stateMutability: 'nonpayable',
99
+ },
100
+ {
101
+ type: 'function',
102
+ name: 'withdraw',
103
+ inputs: [
104
+ { name: 'token', type: 'address' },
105
+ { name: 'to', type: 'address' },
106
+ { name: 'amount', type: 'uint256' },
107
+ { name: 'reason', type: 'string' },
108
+ ],
109
+ outputs: [],
110
+ stateMutability: 'nonpayable',
111
+ },
112
+ {
113
+ type: 'function',
114
+ name: 'setBudget',
115
+ inputs: [
116
+ { name: 'operationsBps', type: 'uint256' },
117
+ { name: 'growthBps', type: 'uint256' },
118
+ { name: 'reservesBps', type: 'uint256' },
119
+ { name: 'yieldBps', type: 'uint256' },
120
+ ],
121
+ outputs: [],
122
+ stateMutability: 'nonpayable',
123
+ },
124
+ {
125
+ type: 'function',
126
+ name: 'budget',
127
+ inputs: [],
128
+ outputs: [
129
+ { name: 'operationsBps', type: 'uint256' },
130
+ { name: 'growthBps', type: 'uint256' },
131
+ { name: 'reservesBps', type: 'uint256' },
132
+ { name: 'yieldBps', type: 'uint256' },
133
+ ],
134
+ stateMutability: 'view',
135
+ },
136
+ {
137
+ type: 'function',
138
+ name: 'getMembers',
139
+ inputs: [],
140
+ outputs: [
141
+ { name: '', type: 'address[]' },
142
+ { name: '', type: 'uint8[]' },
143
+ ],
144
+ stateMutability: 'view',
145
+ },
146
+ {
147
+ type: 'function',
148
+ name: 'grantRole',
149
+ inputs: [
150
+ { name: 'account', type: 'address' },
151
+ { name: 'role', type: 'uint8' },
152
+ ],
153
+ outputs: [],
154
+ stateMutability: 'nonpayable',
155
+ },
156
+ {
157
+ type: 'function',
158
+ name: 'setSpendLimit',
159
+ inputs: [
160
+ { name: 'operator', type: 'address' },
161
+ { name: 'maxPerTx', type: 'uint256' },
162
+ { name: 'maxPerDay', type: 'uint256' },
163
+ ],
164
+ outputs: [],
165
+ stateMutability: 'nonpayable',
166
+ },
167
+ {
168
+ type: 'function',
169
+ name: 'spendLimits',
170
+ inputs: [{ name: '', type: 'address' }],
171
+ outputs: [
172
+ { name: 'maxPerTx', type: 'uint256' },
173
+ { name: 'maxPerDay', type: 'uint256' },
174
+ { name: 'spentToday', type: 'uint256' },
175
+ { name: 'dayStart', type: 'uint256' },
176
+ ],
177
+ stateMutability: 'view',
178
+ },
179
+ ] as const;
180
+
181
+ const ERC20_ABI = [
182
+ {
183
+ type: 'function',
184
+ name: 'approve',
185
+ inputs: [
186
+ { name: 'spender', type: 'address' },
187
+ { name: 'amount', type: 'uint256' },
188
+ ],
189
+ outputs: [{ name: '', type: 'bool' }],
190
+ stateMutability: 'nonpayable',
191
+ },
192
+ {
193
+ type: 'function',
194
+ name: 'symbol',
195
+ inputs: [],
196
+ outputs: [{ name: '', type: 'string' }],
197
+ stateMutability: 'view',
198
+ },
199
+ {
200
+ type: 'function',
201
+ name: 'decimals',
202
+ inputs: [],
203
+ outputs: [{ name: '', type: 'uint8' }],
204
+ stateMutability: 'view',
205
+ },
206
+ ] as const;
207
+
208
+ /**
209
+ * Get treasury address for the connected wallet
210
+ */
211
+ async function getTreasuryAddress(address?: string): Promise<Address> {
212
+ if (address) {
213
+ return address as Address;
214
+ }
215
+
216
+ const walletAddress = getWalletAddress() as Address;
217
+ const publicClient = getPublicClient('mainnet');
218
+
219
+ const treasuryAddress = await publicClient.readContract({
220
+ address: TREASURY_FACTORY_ADDRESS,
221
+ abi: TREASURY_FACTORY_ABI,
222
+ functionName: 'treasuries',
223
+ args: [walletAddress],
224
+ });
225
+
226
+ if (treasuryAddress === '0x0000000000000000000000000000000000000000') {
227
+ throw new Error('No treasury found for connected wallet. Create one with: plob treasury create <name>');
228
+ }
229
+
230
+ return treasuryAddress;
231
+ }
232
+
233
+ /**
234
+ * Format time ago
235
+ */
236
+ function timeAgo(timestamp: bigint): string {
237
+ const now = Math.floor(Date.now() / 1000);
238
+ const secondsAgo = now - Number(timestamp);
239
+
240
+ if (secondsAgo < 60) return `${secondsAgo}s ago`;
241
+ if (secondsAgo < 3600) return `${Math.floor(secondsAgo / 60)}m ago`;
242
+ if (secondsAgo < 86400) return `${Math.floor(secondsAgo / 3600)}h ago`;
243
+ return `${Math.floor(secondsAgo / 86400)}d ago`;
244
+ }
245
+
246
+ /**
247
+ * Get token price in USD (mock for now, can integrate with price oracle)
248
+ */
249
+ async function getTokenPriceUSD(token: Address): Promise<number> {
250
+ // ETH address
251
+ if (token === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE') {
252
+ return 3000; // Mock ETH price
253
+ }
254
+ // USDC
255
+ if (token.toLowerCase() === '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') {
256
+ return 1;
257
+ }
258
+ // Default
259
+ return 0;
260
+ }
261
+
262
+ /**
263
+ * Parse role string to enum
264
+ */
265
+ function parseRole(roleStr: string): Role {
266
+ const normalized = roleStr.toLowerCase();
267
+ switch (normalized) {
268
+ case 'viewer':
269
+ return Role.VIEWER;
270
+ case 'operator':
271
+ return Role.OPERATOR;
272
+ case 'admin':
273
+ return Role.ADMIN;
274
+ default:
275
+ throw new Error(`Invalid role: ${roleStr}. Must be one of: viewer, operator, admin`);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Create treasury command
281
+ */
282
+ export function createTreasuryCommand(): Command {
283
+ const cmd = new Command('treasury')
284
+ .description('Manage agent treasuries on Base');
285
+
286
+ // Create treasury
287
+ cmd
288
+ .command('create')
289
+ .description('Create a new treasury')
290
+ .argument('<name>', 'Treasury name')
291
+ .option('--json', 'Output as JSON')
292
+ .action(async (name: string, options: OutputOptions) => {
293
+ try {
294
+ const { client, account } = await loadWallet();
295
+
296
+ const hash = await withSpinner(
297
+ 'Creating treasury...',
298
+ async () =>
299
+ client.writeContract({
300
+ address: TREASURY_FACTORY_ADDRESS,
301
+ abi: TREASURY_FACTORY_ABI,
302
+ functionName: 'createTreasury',
303
+ args: [name],
304
+ account,
305
+ chain: base,
306
+ })
307
+ );
308
+
309
+ // Wait for transaction and get receipt
310
+ const publicClient = getPublicClient('mainnet');
311
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
312
+
313
+ // Parse logs to get treasury address
314
+ let treasuryAddress: Address | null = null;
315
+ for (const log of receipt.logs) {
316
+ if (log.address.toLowerCase() === TREASURY_FACTORY_ADDRESS.toLowerCase()) {
317
+ try {
318
+ const decoded = decodeEventLog({
319
+ abi: TREASURY_FACTORY_ABI,
320
+ data: log.data,
321
+ topics: log.topics,
322
+ });
323
+ if (decoded.eventName === 'TreasuryCreated') {
324
+ treasuryAddress = (decoded.args as any).treasury;
325
+ break;
326
+ }
327
+ } catch {}
328
+ }
329
+ }
330
+
331
+ if (outputJSON({
332
+ name,
333
+ treasuryAddress,
334
+ transactionHash: hash,
335
+ }, options)) {
336
+ return;
337
+ }
338
+
339
+ console.log();
340
+ success('Treasury created successfully!');
341
+ console.log();
342
+ console.log(' Name: ', chalk.white.bold(name));
343
+ console.log(' Address: ', chalk.cyan(treasuryAddress || 'Check transaction receipt'));
344
+ console.log(' Tx Hash: ', chalk.gray(hash));
345
+ console.log();
346
+ info(`View on BaseScan: https://basescan.org/tx/${hash}`);
347
+ console.log();
348
+ } catch (err) {
349
+ error(`Failed to create treasury: ${err}`);
350
+ process.exit(1);
351
+ }
352
+ });
353
+
354
+ // Get treasury info
355
+ cmd
356
+ .command('info')
357
+ .description('Show treasury summary')
358
+ .argument('[address]', 'Treasury address (defaults to your treasury)')
359
+ .option('--json', 'Output as JSON')
360
+ .action(async (address: string | undefined, options: OutputOptions) => {
361
+ try {
362
+ const treasuryAddress = await getTreasuryAddress(address);
363
+ const publicClient = getPublicClient('mainnet');
364
+
365
+ const summary = await withSpinner(
366
+ 'Fetching treasury info...',
367
+ async () =>
368
+ publicClient.readContract({
369
+ address: treasuryAddress,
370
+ abi: TREASURY_ABI,
371
+ functionName: 'getSummary',
372
+ })
373
+ );
374
+
375
+ const [name, owner, ethBalance, tokenCount, memberCount, totalDeposited, totalWithdrawn, reserveLockBps, createdAt] = summary;
376
+
377
+ if (outputJSON({
378
+ address: treasuryAddress,
379
+ name,
380
+ owner,
381
+ ethBalance: ethBalance.toString(),
382
+ tokenCount: Number(tokenCount),
383
+ memberCount: Number(memberCount),
384
+ totalDeposited: totalDeposited.toString(),
385
+ totalWithdrawn: totalWithdrawn.toString(),
386
+ reserveLockBps: Number(reserveLockBps),
387
+ createdAt: Number(createdAt),
388
+ age: timeAgo(createdAt),
389
+ }, options)) {
390
+ return;
391
+ }
392
+
393
+ console.log();
394
+ console.log(chalk.bold.cyan('🏛️ Treasury Summary'));
395
+ console.log();
396
+ console.log(' Name: ', chalk.white.bold(name));
397
+ console.log(' Address: ', chalk.gray(treasuryAddress));
398
+ console.log(' Owner: ', chalk.gray(owner));
399
+ console.log(' ETH Balance: ', chalk.white(formatUnits(ethBalance, 18)), chalk.gray('ETH'));
400
+ console.log(' Token Count: ', chalk.white(tokenCount.toString()));
401
+ console.log(' Members: ', chalk.white(memberCount.toString()));
402
+ console.log(' Total Deposited:', chalk.green(`$${formatUnits(totalDeposited, 6)}`));
403
+ console.log(' Total Withdrawn:', chalk.yellow(`$${formatUnits(totalWithdrawn, 6)}`));
404
+ console.log(' Reserve Lock: ', chalk.white(`${Number(reserveLockBps) / 100}%`));
405
+ console.log(' Age: ', chalk.gray(timeAgo(createdAt)));
406
+ console.log();
407
+ } catch (err) {
408
+ error(`Failed to get treasury info: ${err}`);
409
+ process.exit(1);
410
+ }
411
+ });
412
+
413
+ // Get balances
414
+ cmd
415
+ .command('balances')
416
+ .description('Show all token balances')
417
+ .argument('[address]', 'Treasury address (defaults to your treasury)')
418
+ .option('--json', 'Output as JSON')
419
+ .action(async (address: string | undefined, options: OutputOptions) => {
420
+ try {
421
+ const treasuryAddress = await getTreasuryAddress(address);
422
+ const publicClient = getPublicClient('mainnet');
423
+
424
+ const balances = await withSpinner(
425
+ 'Fetching balances...',
426
+ async () =>
427
+ publicClient.readContract({
428
+ address: treasuryAddress,
429
+ abi: TREASURY_ABI,
430
+ functionName: 'getBalances',
431
+ })
432
+ );
433
+
434
+ // Get token symbols and decimals
435
+ const enrichedBalances = await Promise.all(
436
+ balances.map(async (bal) => {
437
+ try {
438
+ const [symbol, decimals] = await Promise.all([
439
+ publicClient.readContract({
440
+ address: bal.token,
441
+ abi: ERC20_ABI,
442
+ functionName: 'symbol',
443
+ }),
444
+ publicClient.readContract({
445
+ address: bal.token,
446
+ abi: ERC20_ABI,
447
+ functionName: 'decimals',
448
+ }),
449
+ ]);
450
+
451
+ const amount = formatUnits(bal.balance, decimals);
452
+ const priceUSD = await getTokenPriceUSD(bal.token);
453
+ const valueUSD = parseFloat(amount) * priceUSD;
454
+
455
+ return {
456
+ token: bal.token,
457
+ symbol,
458
+ balance: amount,
459
+ decimals,
460
+ valueUSD,
461
+ };
462
+ } catch {
463
+ return {
464
+ token: bal.token,
465
+ symbol: 'UNKNOWN',
466
+ balance: bal.balance.toString(),
467
+ decimals: 18,
468
+ valueUSD: 0,
469
+ };
470
+ }
471
+ })
472
+ );
473
+
474
+ if (outputJSON({ balances: enrichedBalances }, options)) {
475
+ return;
476
+ }
477
+
478
+ console.log();
479
+ console.log(chalk.bold.cyan('💰 Treasury Balances'));
480
+ console.log();
481
+
482
+ if (enrichedBalances.length === 0) {
483
+ info('No token balances');
484
+ return;
485
+ }
486
+
487
+ const table = createTable({
488
+ head: ['Token', 'Balance', 'Value (USD)'],
489
+ });
490
+
491
+ for (const bal of enrichedBalances) {
492
+ table.push([
493
+ chalk.white.bold(bal.symbol),
494
+ chalk.white(bal.balance),
495
+ chalk.green(`$${bal.valueUSD.toFixed(2)}`),
496
+ ]);
497
+ }
498
+
499
+ console.log(table.toString());
500
+ console.log();
501
+
502
+ const totalValue = enrichedBalances.reduce((sum, bal) => sum + bal.valueUSD, 0);
503
+ console.log(' Total Value: ', chalk.bold.green(`$${totalValue.toFixed(2)}`));
504
+ console.log();
505
+ } catch (err) {
506
+ error(`Failed to get balances: ${err}`);
507
+ process.exit(1);
508
+ }
509
+ });
510
+
511
+ // Deposit
512
+ cmd
513
+ .command('deposit')
514
+ .description('Deposit tokens into treasury')
515
+ .requiredOption('--token <address>', 'Token address')
516
+ .requiredOption('--amount <amount>', 'Amount to deposit')
517
+ .option('--json', 'Output as JSON')
518
+ .action(async (options: {
519
+ token: string;
520
+ amount: string;
521
+ } & OutputOptions) => {
522
+ try {
523
+ const treasuryAddress = await getTreasuryAddress();
524
+ const { client, account } = await loadWallet();
525
+ const publicClient = getPublicClient('mainnet');
526
+
527
+ const tokenAddress = options.token as Address;
528
+
529
+ // Get token decimals
530
+ const decimals = await publicClient.readContract({
531
+ address: tokenAddress,
532
+ abi: ERC20_ABI,
533
+ functionName: 'decimals',
534
+ });
535
+
536
+ const amount = parseUnits(options.amount, decimals);
537
+
538
+ // First approve token
539
+ const approveHash = await withSpinner(
540
+ 'Approving token...',
541
+ async () =>
542
+ client.writeContract({
543
+ address: tokenAddress,
544
+ abi: ERC20_ABI,
545
+ functionName: 'approve',
546
+ args: [treasuryAddress, amount],
547
+ account,
548
+ chain: base,
549
+ })
550
+ );
551
+
552
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
553
+
554
+ // Deposit
555
+ const depositHash = await withSpinner(
556
+ 'Depositing tokens...',
557
+ async () =>
558
+ client.writeContract({
559
+ address: treasuryAddress,
560
+ abi: TREASURY_ABI,
561
+ functionName: 'deposit',
562
+ args: [tokenAddress, amount],
563
+ account,
564
+ chain: base,
565
+ })
566
+ );
567
+
568
+ await publicClient.waitForTransactionReceipt({ hash: depositHash });
569
+
570
+ if (outputJSON({
571
+ token: tokenAddress,
572
+ amount: options.amount,
573
+ transactionHash: depositHash,
574
+ }, options)) {
575
+ return;
576
+ }
577
+
578
+ console.log();
579
+ success('Deposit successful!');
580
+ console.log();
581
+ console.log(' Amount: ', chalk.white.bold(options.amount));
582
+ console.log(' Token: ', chalk.gray(tokenAddress));
583
+ console.log(' Tx Hash: ', chalk.gray(depositHash));
584
+ console.log();
585
+ } catch (err) {
586
+ error(`Failed to deposit: ${err}`);
587
+ process.exit(1);
588
+ }
589
+ });
590
+
591
+ // Withdraw
592
+ cmd
593
+ .command('withdraw')
594
+ .description('Withdraw tokens from treasury')
595
+ .requiredOption('--token <address>', 'Token address')
596
+ .requiredOption('--to <address>', 'Recipient address')
597
+ .requiredOption('--amount <amount>', 'Amount to withdraw')
598
+ .requiredOption('--reason <string>', 'Reason for withdrawal')
599
+ .option('--json', 'Output as JSON')
600
+ .action(async (options: {
601
+ token: string;
602
+ to: string;
603
+ amount: string;
604
+ reason: string;
605
+ } & OutputOptions) => {
606
+ try {
607
+ const treasuryAddress = await getTreasuryAddress();
608
+ const { client, account } = await loadWallet();
609
+ const publicClient = getPublicClient('mainnet');
610
+
611
+ const tokenAddress = options.token as Address;
612
+ const toAddress = options.to as Address;
613
+
614
+ // Get token decimals
615
+ const decimals = await publicClient.readContract({
616
+ address: tokenAddress,
617
+ abi: ERC20_ABI,
618
+ functionName: 'decimals',
619
+ });
620
+
621
+ const amount = parseUnits(options.amount, decimals);
622
+
623
+ const hash = await withSpinner(
624
+ 'Withdrawing tokens...',
625
+ async () =>
626
+ client.writeContract({
627
+ address: treasuryAddress,
628
+ abi: TREASURY_ABI,
629
+ functionName: 'withdraw',
630
+ args: [tokenAddress, toAddress, amount, options.reason],
631
+ account,
632
+ chain: base,
633
+ })
634
+ );
635
+
636
+ await publicClient.waitForTransactionReceipt({ hash });
637
+
638
+ if (outputJSON({
639
+ token: tokenAddress,
640
+ to: toAddress,
641
+ amount: options.amount,
642
+ reason: options.reason,
643
+ transactionHash: hash,
644
+ }, options)) {
645
+ return;
646
+ }
647
+
648
+ console.log();
649
+ success('Withdrawal successful!');
650
+ console.log();
651
+ console.log(' Amount: ', chalk.white.bold(options.amount));
652
+ console.log(' To: ', chalk.gray(toAddress));
653
+ console.log(' Reason: ', chalk.white(options.reason));
654
+ console.log(' Tx Hash: ', chalk.gray(hash));
655
+ console.log();
656
+ } catch (err) {
657
+ error(`Failed to withdraw: ${err}`);
658
+ process.exit(1);
659
+ }
660
+ });
661
+
662
+ // Set budget
663
+ cmd
664
+ .command('budget')
665
+ .description('Set budget allocation (must total 10000 bps)')
666
+ .requiredOption('--ops <bps>', 'Operations allocation in basis points')
667
+ .requiredOption('--growth <bps>', 'Growth allocation in basis points')
668
+ .requiredOption('--reserves <bps>', 'Reserves allocation in basis points')
669
+ .requiredOption('--yield <bps>', 'Yield allocation in basis points')
670
+ .option('--json', 'Output as JSON')
671
+ .action(async (options: {
672
+ ops: string;
673
+ growth: string;
674
+ reserves: string;
675
+ yield: string;
676
+ } & OutputOptions) => {
677
+ try {
678
+ const opsBps = BigInt(options.ops);
679
+ const growthBps = BigInt(options.growth);
680
+ const reservesBps = BigInt(options.reserves);
681
+ const yieldBps = BigInt(options.yield);
682
+
683
+ const total = opsBps + growthBps + reservesBps + yieldBps;
684
+ if (total !== 10000n) {
685
+ throw new Error(`Budget allocations must total 10000 bps. Got: ${total}`);
686
+ }
687
+
688
+ const treasuryAddress = await getTreasuryAddress();
689
+ const { client, account } = await loadWallet();
690
+ const publicClient = getPublicClient('mainnet');
691
+
692
+ const hash = await withSpinner(
693
+ 'Setting budget allocation...',
694
+ async () =>
695
+ client.writeContract({
696
+ address: treasuryAddress,
697
+ abi: TREASURY_ABI,
698
+ functionName: 'setBudget',
699
+ args: [opsBps, growthBps, reservesBps, yieldBps],
700
+ account,
701
+ chain: base,
702
+ })
703
+ );
704
+
705
+ await publicClient.waitForTransactionReceipt({ hash });
706
+
707
+ if (outputJSON({
708
+ operations: Number(opsBps),
709
+ growth: Number(growthBps),
710
+ reserves: Number(reservesBps),
711
+ yield: Number(yieldBps),
712
+ transactionHash: hash,
713
+ }, options)) {
714
+ return;
715
+ }
716
+
717
+ console.log();
718
+ success('Budget allocation updated!');
719
+ console.log();
720
+ console.log(' Operations: ', chalk.white(`${Number(opsBps) / 100}%`));
721
+ console.log(' Growth: ', chalk.white(`${Number(growthBps) / 100}%`));
722
+ console.log(' Reserves: ', chalk.white(`${Number(reservesBps) / 100}%`));
723
+ console.log(' Yield: ', chalk.white(`${Number(yieldBps) / 100}%`));
724
+ console.log();
725
+ } catch (err) {
726
+ error(`Failed to set budget: ${err}`);
727
+ process.exit(1);
728
+ }
729
+ });
730
+
731
+ // List members
732
+ cmd
733
+ .command('members')
734
+ .description('List all treasury members with roles')
735
+ .argument('[address]', 'Treasury address (defaults to your treasury)')
736
+ .option('--json', 'Output as JSON')
737
+ .action(async (address: string | undefined, options: OutputOptions) => {
738
+ try {
739
+ const treasuryAddress = await getTreasuryAddress(address);
740
+ const publicClient = getPublicClient('mainnet');
741
+
742
+ const [addresses, roles] = await withSpinner(
743
+ 'Fetching members...',
744
+ async () =>
745
+ publicClient.readContract({
746
+ address: treasuryAddress,
747
+ abi: TREASURY_ABI,
748
+ functionName: 'getMembers',
749
+ })
750
+ );
751
+
752
+ const members = addresses.map((addr, i) => ({
753
+ address: addr,
754
+ role: ROLE_NAMES[roles[i]] || 'Unknown',
755
+ roleValue: roles[i],
756
+ }));
757
+
758
+ if (outputJSON({ members }, options)) {
759
+ return;
760
+ }
761
+
762
+ console.log();
763
+ console.log(chalk.bold.cyan('👥 Treasury Members'));
764
+ console.log();
765
+
766
+ if (members.length === 0) {
767
+ info('No members found');
768
+ return;
769
+ }
770
+
771
+ const table = createTable({
772
+ head: ['Address', 'Role'],
773
+ });
774
+
775
+ for (const member of members) {
776
+ const roleColor = member.roleValue === 3 ? chalk.red : member.roleValue === 2 ? chalk.yellow : chalk.blue;
777
+ table.push([
778
+ chalk.gray(member.address),
779
+ roleColor(member.role),
780
+ ]);
781
+ }
782
+
783
+ console.log(table.toString());
784
+ console.log();
785
+ } catch (err) {
786
+ error(`Failed to get members: ${err}`);
787
+ process.exit(1);
788
+ }
789
+ });
790
+
791
+ // Grant role
792
+ cmd
793
+ .command('grant')
794
+ .description('Grant role to an address')
795
+ .requiredOption('--address <addr>', 'Address to grant role')
796
+ .requiredOption('--role <role>', 'Role to grant (viewer|operator|admin)')
797
+ .option('--json', 'Output as JSON')
798
+ .action(async (options: {
799
+ address: string;
800
+ role: string;
801
+ } & OutputOptions) => {
802
+ try {
803
+ const roleEnum = parseRole(options.role);
804
+ const memberAddress = options.address as Address;
805
+ const treasuryAddress = await getTreasuryAddress();
806
+ const { client, account } = await loadWallet();
807
+ const publicClient = getPublicClient('mainnet');
808
+
809
+ const hash = await withSpinner(
810
+ 'Granting role...',
811
+ async () =>
812
+ client.writeContract({
813
+ address: treasuryAddress,
814
+ abi: TREASURY_ABI,
815
+ functionName: 'grantRole',
816
+ args: [memberAddress, roleEnum],
817
+ account,
818
+ chain: base,
819
+ })
820
+ );
821
+
822
+ await publicClient.waitForTransactionReceipt({ hash });
823
+
824
+ if (outputJSON({
825
+ address: memberAddress,
826
+ role: options.role,
827
+ transactionHash: hash,
828
+ }, options)) {
829
+ return;
830
+ }
831
+
832
+ console.log();
833
+ success('Role granted successfully!');
834
+ console.log();
835
+ console.log(' Address: ', chalk.gray(memberAddress));
836
+ console.log(' Role: ', chalk.white.bold(options.role));
837
+ console.log(' Tx Hash: ', chalk.gray(hash));
838
+ console.log();
839
+ } catch (err) {
840
+ error(`Failed to grant role: ${err}`);
841
+ process.exit(1);
842
+ }
843
+ });
844
+
845
+ // Set spend limit
846
+ cmd
847
+ .command('limit')
848
+ .description('Set spending limits for an operator')
849
+ .requiredOption('--address <addr>', 'Operator address')
850
+ .requiredOption('--per-tx <amount>', 'Max amount per transaction (in USD)')
851
+ .requiredOption('--per-day <amount>', 'Max amount per day (in USD)')
852
+ .option('--json', 'Output as JSON')
853
+ .action(async (options: {
854
+ address: string;
855
+ perTx: string;
856
+ perDay: string;
857
+ } & OutputOptions) => {
858
+ try {
859
+ const operatorAddress = options.address as Address;
860
+ const treasuryAddress = await getTreasuryAddress();
861
+ const { client, account } = await loadWallet();
862
+ const publicClient = getPublicClient('mainnet');
863
+
864
+ // Convert USD amounts to USDC wei (6 decimals)
865
+ const maxPerTx = parseUnits(options.perTx, 6);
866
+ const maxPerDay = parseUnits(options.perDay, 6);
867
+
868
+ const hash = await withSpinner(
869
+ 'Setting spend limits...',
870
+ async () =>
871
+ client.writeContract({
872
+ address: treasuryAddress,
873
+ abi: TREASURY_ABI,
874
+ functionName: 'setSpendLimit',
875
+ args: [operatorAddress, maxPerTx, maxPerDay],
876
+ account,
877
+ chain: base,
878
+ })
879
+ );
880
+
881
+ await publicClient.waitForTransactionReceipt({ hash });
882
+
883
+ if (outputJSON({
884
+ operator: operatorAddress,
885
+ maxPerTx: options.perTx,
886
+ maxPerDay: options.perDay,
887
+ transactionHash: hash,
888
+ }, options)) {
889
+ return;
890
+ }
891
+
892
+ console.log();
893
+ success('Spend limits set successfully!');
894
+ console.log();
895
+ console.log(' Operator: ', chalk.gray(operatorAddress));
896
+ console.log(' Max Per Tx: ', chalk.white.bold(`$${options.perTx}`));
897
+ console.log(' Max Per Day: ', chalk.white.bold(`$${options.perDay}`));
898
+ console.log(' Tx Hash: ', chalk.gray(hash));
899
+ console.log();
900
+ } catch (err) {
901
+ error(`Failed to set spend limits: ${err}`);
902
+ process.exit(1);
903
+ }
904
+ });
905
+
906
+ return cmd;
907
+ }