@paylobster/cli 4.3.0 → 4.5.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.
Files changed (54) hide show
  1. package/README.md +82 -40
  2. package/dist/src/commands/dashboard.d.ts +3 -0
  3. package/dist/src/commands/dashboard.d.ts.map +1 -0
  4. package/dist/src/commands/dashboard.js +67 -0
  5. package/dist/src/commands/dashboard.js.map +1 -0
  6. package/dist/src/commands/export.d.ts +3 -0
  7. package/dist/src/commands/export.d.ts.map +1 -0
  8. package/dist/src/commands/export.js +97 -0
  9. package/dist/src/commands/export.js.map +1 -0
  10. package/dist/src/commands/health.d.ts +3 -0
  11. package/dist/src/commands/health.d.ts.map +1 -0
  12. package/dist/src/commands/health.js +165 -0
  13. package/dist/src/commands/health.js.map +1 -0
  14. package/dist/src/commands/invest.d.ts +3 -0
  15. package/dist/src/commands/invest.d.ts.map +1 -0
  16. package/dist/src/commands/invest.js +721 -0
  17. package/dist/src/commands/invest.js.map +1 -0
  18. package/dist/src/commands/quickstart.d.ts +3 -0
  19. package/dist/src/commands/quickstart.d.ts.map +1 -0
  20. package/dist/src/commands/quickstart.js +138 -0
  21. package/dist/src/commands/quickstart.js.map +1 -0
  22. package/dist/src/commands/search.d.ts +3 -0
  23. package/dist/src/commands/search.d.ts.map +1 -0
  24. package/dist/src/commands/search.js +126 -0
  25. package/dist/src/commands/search.js.map +1 -0
  26. package/dist/src/commands/treasury.d.ts.map +1 -1
  27. package/dist/src/commands/treasury.js +317 -0
  28. package/dist/src/commands/treasury.js.map +1 -1
  29. package/dist/src/commands/trust-graph.d.ts +3 -0
  30. package/dist/src/commands/trust-graph.d.ts.map +1 -0
  31. package/dist/src/commands/trust-graph.js +282 -0
  32. package/dist/src/commands/trust-graph.js.map +1 -0
  33. package/dist/src/commands/whoami.d.ts +3 -0
  34. package/dist/src/commands/whoami.d.ts.map +1 -0
  35. package/dist/src/commands/whoami.js +160 -0
  36. package/dist/src/commands/whoami.js.map +1 -0
  37. package/dist/src/index.js +18 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/lib/contracts.d.ts +177 -23
  40. package/dist/src/lib/contracts.d.ts.map +1 -1
  41. package/dist/src/lib/contracts.js +151 -0
  42. package/dist/src/lib/contracts.js.map +1 -1
  43. package/package.json +4 -2
  44. package/src/commands/dashboard.ts +69 -0
  45. package/src/commands/export.ts +132 -0
  46. package/src/commands/health.ts +212 -0
  47. package/src/commands/invest.ts +810 -0
  48. package/src/commands/quickstart.ts +150 -0
  49. package/src/commands/search.ts +151 -0
  50. package/src/commands/treasury.ts +385 -0
  51. package/src/commands/trust-graph.ts +267 -0
  52. package/src/commands/whoami.ts +172 -0
  53. package/src/index.ts +18 -2
  54. package/src/lib/contracts.ts +159 -0
@@ -0,0 +1,150 @@
1
+ import { Command } from 'commander';
2
+ import { getAgentInfo, registerAgent } from '../lib/contracts';
3
+ import { getWalletAddress } from '../lib/wallet';
4
+ import { error, success } from '../lib/display';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import readline from 'readline';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ function prompt(question: string): Promise<string> {
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+
19
+ return new Promise((resolve) => {
20
+ rl.question(question, (answer) => {
21
+ rl.close();
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
26
+
27
+ export function createQuickstartCommand(): Command {
28
+ const cmd = new Command('quickstart')
29
+ .description('šŸš€ Interactive wizard to get started with PayLobster')
30
+ .action(async () => {
31
+ console.log(chalk.cyan.bold('\nšŸ¦ž PayLobster Quickstart Wizard\n'));
32
+ console.log(chalk.gray('Let\'s get you set up with PayLobster in just a few steps.\n'));
33
+
34
+ try {
35
+ // Step 1: Check wallet configuration
36
+ console.log(chalk.cyan('Step 1/5:'), chalk.bold('Wallet Configuration'));
37
+
38
+ let address: `0x${string}`;
39
+ try {
40
+ address = getWalletAddress() as `0x${string}`;
41
+ } catch (err) {
42
+ console.log(chalk.yellow('āš ļø No wallet configured yet.'));
43
+ console.log('Please run:', chalk.green('plob auth setup'));
44
+ console.log('Then come back and run', chalk.green('plob quickstart'), 'again.\n');
45
+ return;
46
+ }
47
+ console.log(chalk.green('āœ“'), 'Wallet configured:', chalk.gray(address));
48
+
49
+ // Check if already registered
50
+ const spinner = ora('Checking registration status...').start();
51
+ const agentInfo = await getAgentInfo(address);
52
+ spinner.stop();
53
+
54
+ if (agentInfo.registered) {
55
+ console.log(chalk.green('āœ“'), 'Already registered as:', chalk.bold(agentInfo.name));
56
+ console.log(chalk.yellow('\nYou\'re all set! Run'), chalk.green('plob whoami'), chalk.yellow('to see your profile.\n'));
57
+ return;
58
+ }
59
+
60
+ console.log(chalk.gray('Not registered yet. Let\'s continue...\n'));
61
+
62
+ // Step 2: Register agent identity
63
+ console.log(chalk.cyan('Step 2/5:'), chalk.bold('Agent Identity'));
64
+
65
+ const name = await prompt(chalk.white('Agent name: '));
66
+ if (!name) {
67
+ console.log(chalk.red('Name is required. Exiting.\n'));
68
+ return;
69
+ }
70
+
71
+ const capabilities = await prompt(chalk.white('Capabilities (comma-separated, e.g., "code review, testing, docs"): '));
72
+ if (!capabilities) {
73
+ console.log(chalk.red('Capabilities are required. Exiting.\n'));
74
+ return;
75
+ }
76
+
77
+ spinner.start('Registering agent identity...');
78
+ try {
79
+ const tokenId = await registerAgent(name, capabilities);
80
+ spinner.succeed(chalk.green('āœ“ Agent registered! Token ID: ') + chalk.bold(tokenId.toString()));
81
+ } catch (err: any) {
82
+ spinner.fail(chalk.red('Failed to register agent'));
83
+ error(`Registration error: ${err.message}`);
84
+ return;
85
+ }
86
+
87
+ // Step 3: Create treasury
88
+ console.log(chalk.cyan('\nStep 3/5:'), chalk.bold('Treasury Setup'));
89
+ console.log(chalk.gray('A treasury manages your funds with budgets and spending rules.'));
90
+
91
+ const createTreasury = await prompt(chalk.white('Create a treasury? (y/n): '));
92
+
93
+ if (createTreasury.toLowerCase() === 'y') {
94
+ const treasuryName = await prompt(chalk.white('Treasury name: ')) || `${name} Treasury`;
95
+
96
+ spinner.start('Creating treasury...');
97
+ try {
98
+ await execAsync(`plob treasury create --name "${treasuryName}"`);
99
+ spinner.succeed(chalk.green('āœ“ Treasury created: ') + chalk.bold(treasuryName));
100
+ } catch (err: any) {
101
+ spinner.fail(chalk.red('Failed to create treasury'));
102
+ console.log(chalk.yellow('You can create one later with:'), chalk.green('plob treasury create'));
103
+ }
104
+
105
+ // Step 4: Set default budget
106
+ console.log(chalk.cyan('\nStep 4/5:'), chalk.bold('Budget Allocation'));
107
+ console.log(chalk.gray('Recommended: 40% operations, 30% growth, 20% reserve, 10% rewards'));
108
+
109
+ const setBudget = await prompt(chalk.white('Use default budget allocation? (y/n): '));
110
+
111
+ if (setBudget.toLowerCase() === 'y') {
112
+ spinner.start('Setting budget...');
113
+ try {
114
+ await execAsync('plob treasury budget --operations 40 --growth 30 --reserve 20 --rewards 10');
115
+ spinner.succeed(chalk.green('āœ“ Budget configured'));
116
+ } catch (err: any) {
117
+ spinner.fail(chalk.red('Failed to set budget'));
118
+ console.log(chalk.yellow('You can set it later with:'), chalk.green('plob treasury budget'));
119
+ }
120
+ }
121
+ } else {
122
+ console.log(chalk.gray('Skipped treasury setup.'));
123
+ }
124
+
125
+ // Step 5: Summary
126
+ console.log(chalk.cyan('\nStep 5/5:'), chalk.bold('Summary'));
127
+ console.log(chalk.green.bold('\nšŸŽ‰ You\'re all set!\n'));
128
+
129
+ console.log(chalk.bold('What you created:'));
130
+ console.log(chalk.green(' āœ“'), 'Agent identity:', chalk.bold(name));
131
+ console.log(chalk.green(' āœ“'), 'Capabilities:', chalk.gray(capabilities));
132
+ if (createTreasury.toLowerCase() === 'y') {
133
+ console.log(chalk.green(' āœ“'), 'Treasury with budget allocation');
134
+ }
135
+
136
+ console.log(chalk.bold('\nNext steps:'));
137
+ console.log(chalk.cyan(' •'), 'View your profile:', chalk.green('plob whoami'));
138
+ console.log(chalk.cyan(' •'), 'Check system health:', chalk.green('plob health'));
139
+ console.log(chalk.cyan(' •'), 'Open dashboard:', chalk.green('plob dashboard'));
140
+ console.log(chalk.cyan(' •'), 'Make your first payment:', chalk.green('plob pay <address> <amount>'));
141
+ console.log();
142
+
143
+ } catch (err: any) {
144
+ error(`Quickstart failed: ${err.message}`);
145
+ process.exit(1);
146
+ }
147
+ });
148
+
149
+ return cmd;
150
+ }
@@ -0,0 +1,151 @@
1
+ import { Command } from 'commander';
2
+ import { getPublicClient, getContracts, getAgentInfo, getReputation } from '../lib/contracts';
3
+ import { loadConfig } from '../lib/config';
4
+ import { error, outputJSON } from '../lib/display';
5
+ import type { OutputOptions } from '../lib/types';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import type { Address } from 'viem';
9
+
10
+ interface AgentSearchResult {
11
+ address: Address;
12
+ name: string;
13
+ capabilities: string;
14
+ reputationScore: number;
15
+ tokenId: string;
16
+ }
17
+
18
+ // ABI for AgentRegistered event
19
+ const AGENT_REGISTERED_EVENT = {
20
+ anonymous: false,
21
+ inputs: [
22
+ { indexed: true, name: 'agent', type: 'address' },
23
+ { indexed: true, name: 'tokenId', type: 'uint256' },
24
+ { indexed: false, name: 'name', type: 'string' },
25
+ { indexed: false, name: 'capabilities', type: 'string' },
26
+ ],
27
+ name: 'AgentRegistered',
28
+ type: 'event',
29
+ } as const;
30
+
31
+ export function createSearchCommand(): Command {
32
+ const cmd = new Command('search')
33
+ .description('šŸ” Search for agents by name or capability')
34
+ .argument('<query>', 'Search query (name or capability)')
35
+ .option('--json', 'Output as JSON')
36
+ .option('--limit <number>', 'Maximum number of results', '10')
37
+ .action(async (query: string, options: OutputOptions & { limit?: string }) => {
38
+ const spinner = ora('Searching for agents...').start();
39
+
40
+ try {
41
+ const config = loadConfig();
42
+ const client = getPublicClient(config.network);
43
+ const contracts = getContracts(config.network);
44
+ const limit = parseInt(options.limit || '10');
45
+
46
+ // Get AgentRegistered events
47
+ // Note: This will scan the entire chain history which may be slow
48
+ // In production, you'd want to use a subgraph or indexer
49
+ const logs = await client.getLogs({
50
+ address: contracts.IDENTITY,
51
+ event: AGENT_REGISTERED_EVENT,
52
+ fromBlock: 'earliest',
53
+ toBlock: 'latest',
54
+ });
55
+
56
+ spinner.text = 'Processing results...';
57
+
58
+ // Filter by query (case-insensitive)
59
+ const queryLower = query.toLowerCase();
60
+ const matchingLogs = logs.filter((log) => {
61
+ const name = (log.args.name || '').toLowerCase();
62
+ const capabilities = (log.args.capabilities || '').toLowerCase();
63
+ return name.includes(queryLower) || capabilities.includes(queryLower);
64
+ });
65
+
66
+ // Limit results
67
+ const limitedLogs = matchingLogs.slice(0, limit);
68
+
69
+ // Fetch reputation for each agent
70
+ const results: AgentSearchResult[] = await Promise.all(
71
+ limitedLogs.map(async (log) => {
72
+ const address = log.args.agent!;
73
+ const reputation = await getReputation(address);
74
+
75
+ return {
76
+ address,
77
+ name: log.args.name || 'Unknown',
78
+ capabilities: log.args.capabilities || 'None',
79
+ reputationScore: Number(reputation.score),
80
+ tokenId: log.args.tokenId?.toString() || '0',
81
+ };
82
+ })
83
+ );
84
+
85
+ spinner.stop();
86
+
87
+ // JSON output
88
+ if (outputJSON({
89
+ query,
90
+ results,
91
+ total: matchingLogs.length,
92
+ showing: results.length,
93
+ }, options)) {
94
+ return;
95
+ }
96
+
97
+ // Pretty output
98
+ console.log('\n' + chalk.cyan.bold('═══════════════════════════════════════════════════════════'));
99
+ console.log(chalk.cyan.bold(' šŸ” Agent Search'));
100
+ console.log(chalk.cyan.bold('═══════════════════════════════════════════════════════════\n'));
101
+
102
+ console.log(chalk.gray(' Query: '), chalk.white(query));
103
+ console.log(chalk.gray(' Results: '), chalk.white(`${results.length} / ${matchingLogs.length}`));
104
+ console.log();
105
+
106
+ if (results.length === 0) {
107
+ console.log(chalk.yellow(' No agents found matching your query.'));
108
+ console.log();
109
+ return;
110
+ }
111
+
112
+ // Display results
113
+ for (let i = 0; i < results.length; i++) {
114
+ const agent = results[i];
115
+
116
+ let reputationColor = chalk.red;
117
+ if (agent.reputationScore >= 80) reputationColor = chalk.green;
118
+ else if (agent.reputationScore >= 50) reputationColor = chalk.yellow;
119
+
120
+ console.log(chalk.bold(` ${i + 1}. ${agent.name}`), chalk.gray(`(#${agent.tokenId})`));
121
+ console.log(chalk.gray(' Address: '), chalk.dim(agent.address));
122
+ console.log(chalk.gray(' Reputation: '), reputationColor(agent.reputationScore.toString()));
123
+ console.log(chalk.gray(' Capabilities: '), chalk.white(agent.capabilities));
124
+ console.log();
125
+ }
126
+
127
+ if (matchingLogs.length > results.length) {
128
+ console.log(chalk.gray(` ... and ${matchingLogs.length - results.length} more results`));
129
+ console.log(chalk.gray(' Use --limit to see more'));
130
+ console.log();
131
+ }
132
+
133
+ console.log(chalk.cyan.bold('═══════════════════════════════════════════════════════════\n'));
134
+
135
+ } catch (err: any) {
136
+ spinner.stop();
137
+ error(`Search failed: ${err.message}`);
138
+
139
+ // Provide helpful error message for common issues
140
+ if (err.message.includes('block range')) {
141
+ console.log(chalk.yellow('\nāš ļø The search requires scanning many blocks.'));
142
+ console.log(chalk.gray('This may be slow or hit rate limits with public RPCs.'));
143
+ console.log(chalk.gray('Consider using a dedicated RPC endpoint or subgraph.\n'));
144
+ }
145
+
146
+ process.exit(1);
147
+ }
148
+ });
149
+
150
+ return cmd;
151
+ }
@@ -176,6 +176,52 @@ const TREASURY_ABI = [
176
176
  ],
177
177
  stateMutability: 'view',
178
178
  },
179
+ {
180
+ type: 'function',
181
+ name: 'revokeRole',
182
+ inputs: [{ name: 'account', type: 'address' }],
183
+ outputs: [],
184
+ stateMutability: 'nonpayable',
185
+ },
186
+ {
187
+ type: 'function',
188
+ name: 'setReserveLock',
189
+ inputs: [{ name: 'newLockBps', type: 'uint256' }],
190
+ outputs: [],
191
+ stateMutability: 'nonpayable',
192
+ },
193
+ {
194
+ type: 'function',
195
+ name: 'approveSpender',
196
+ inputs: [
197
+ { name: 'token', type: 'address' },
198
+ { name: 'spender', type: 'address' },
199
+ { name: 'amount', type: 'uint256' },
200
+ ],
201
+ outputs: [],
202
+ stateMutability: 'nonpayable',
203
+ },
204
+ {
205
+ type: 'function',
206
+ name: 'setIntegration',
207
+ inputs: [
208
+ { name: 'integrationName', type: 'string' },
209
+ { name: 'addr', type: 'address' },
210
+ ],
211
+ outputs: [],
212
+ stateMutability: 'nonpayable',
213
+ },
214
+ {
215
+ type: 'function',
216
+ name: 'withdrawETH',
217
+ inputs: [
218
+ { name: 'to', type: 'address' },
219
+ { name: 'amount', type: 'uint256' },
220
+ { name: 'reason', type: 'string' },
221
+ ],
222
+ outputs: [],
223
+ stateMutability: 'nonpayable',
224
+ },
179
225
  ] as const;
180
226
 
181
227
  const ERC20_ABI = [
@@ -903,5 +949,344 @@ export function createTreasuryCommand(): Command {
903
949
  }
904
950
  });
905
951
 
952
+ // Revoke role
953
+ cmd
954
+ .command('revoke')
955
+ .description('Revoke role from an address')
956
+ .requiredOption('--address <addr>', 'Address to revoke role from')
957
+ .option('--json', 'Output as JSON')
958
+ .action(async (options: {
959
+ address: string;
960
+ } & OutputOptions) => {
961
+ try {
962
+ const accountAddress = options.address as Address;
963
+ const treasuryAddress = await getTreasuryAddress();
964
+ const { client, account } = await loadWallet();
965
+ const publicClient = getPublicClient('mainnet');
966
+
967
+ const hash = await withSpinner(
968
+ 'Revoking role...',
969
+ async () =>
970
+ client.writeContract({
971
+ address: treasuryAddress,
972
+ abi: TREASURY_ABI,
973
+ functionName: 'revokeRole',
974
+ args: [accountAddress],
975
+ account,
976
+ chain: base,
977
+ })
978
+ );
979
+
980
+ await publicClient.waitForTransactionReceipt({ hash });
981
+
982
+ if (outputJSON({
983
+ address: accountAddress,
984
+ transactionHash: hash,
985
+ }, options)) {
986
+ return;
987
+ }
988
+
989
+ console.log();
990
+ success('Role revoked successfully!');
991
+ console.log();
992
+ console.log(' Address: ', chalk.gray(accountAddress));
993
+ console.log(' Tx Hash: ', chalk.gray(hash));
994
+ console.log();
995
+ } catch (err) {
996
+ error(`Failed to revoke role: ${err}`);
997
+ process.exit(1);
998
+ }
999
+ });
1000
+
1001
+ // Set reserve lock
1002
+ cmd
1003
+ .command('reserve')
1004
+ .description('Set reserve lock percentage (0-5000 bps)')
1005
+ .requiredOption('--lock <bps>', 'Reserve lock in basis points (0-5000)')
1006
+ .option('--json', 'Output as JSON')
1007
+ .action(async (options: {
1008
+ lock: string;
1009
+ } & OutputOptions) => {
1010
+ try {
1011
+ const lockBps = BigInt(options.lock);
1012
+ if (lockBps < 0n || lockBps > 5000n) {
1013
+ throw new Error('Reserve lock must be between 0 and 5000 bps (0-50%)');
1014
+ }
1015
+
1016
+ const treasuryAddress = await getTreasuryAddress();
1017
+ const { client, account } = await loadWallet();
1018
+ const publicClient = getPublicClient('mainnet');
1019
+
1020
+ const hash = await withSpinner(
1021
+ 'Setting reserve lock...',
1022
+ async () =>
1023
+ client.writeContract({
1024
+ address: treasuryAddress,
1025
+ abi: TREASURY_ABI,
1026
+ functionName: 'setReserveLock',
1027
+ args: [lockBps],
1028
+ account,
1029
+ chain: base,
1030
+ })
1031
+ );
1032
+
1033
+ await publicClient.waitForTransactionReceipt({ hash });
1034
+
1035
+ if (outputJSON({
1036
+ lockBps: Number(lockBps),
1037
+ lockPercent: Number(lockBps) / 100,
1038
+ transactionHash: hash,
1039
+ }, options)) {
1040
+ return;
1041
+ }
1042
+
1043
+ console.log();
1044
+ success('Reserve lock set successfully!');
1045
+ console.log();
1046
+ console.log(' Lock: ', chalk.white.bold(`${Number(lockBps) / 100}%`));
1047
+ console.log(' Tx Hash: ', chalk.gray(hash));
1048
+ console.log();
1049
+ } catch (err) {
1050
+ error(`Failed to set reserve lock: ${err}`);
1051
+ process.exit(1);
1052
+ }
1053
+ });
1054
+
1055
+ // Approve spender
1056
+ cmd
1057
+ .command('approve')
1058
+ .description('Approve token spender')
1059
+ .requiredOption('--token <addr>', 'Token address')
1060
+ .requiredOption('--spender <addr>', 'Spender address')
1061
+ .requiredOption('--amount <amount>', 'Amount to approve')
1062
+ .option('--json', 'Output as JSON')
1063
+ .action(async (options: {
1064
+ token: string;
1065
+ spender: string;
1066
+ amount: string;
1067
+ } & OutputOptions) => {
1068
+ try {
1069
+ const tokenAddress = options.token as Address;
1070
+ const spenderAddress = options.spender as Address;
1071
+ const treasuryAddress = await getTreasuryAddress();
1072
+ const { client, account } = await loadWallet();
1073
+ const publicClient = getPublicClient('mainnet');
1074
+
1075
+ // Get token decimals
1076
+ const decimals = await publicClient.readContract({
1077
+ address: tokenAddress,
1078
+ abi: ERC20_ABI,
1079
+ functionName: 'decimals',
1080
+ });
1081
+
1082
+ const amount = parseUnits(options.amount, decimals);
1083
+
1084
+ const hash = await withSpinner(
1085
+ 'Approving spender...',
1086
+ async () =>
1087
+ client.writeContract({
1088
+ address: treasuryAddress,
1089
+ abi: TREASURY_ABI,
1090
+ functionName: 'approveSpender',
1091
+ args: [tokenAddress, spenderAddress, amount],
1092
+ account,
1093
+ chain: base,
1094
+ })
1095
+ );
1096
+
1097
+ await publicClient.waitForTransactionReceipt({ hash });
1098
+
1099
+ if (outputJSON({
1100
+ token: tokenAddress,
1101
+ spender: spenderAddress,
1102
+ amount: options.amount,
1103
+ transactionHash: hash,
1104
+ }, options)) {
1105
+ return;
1106
+ }
1107
+
1108
+ console.log();
1109
+ success('Spender approved successfully!');
1110
+ console.log();
1111
+ console.log(' Token: ', chalk.gray(tokenAddress));
1112
+ console.log(' Spender: ', chalk.gray(spenderAddress));
1113
+ console.log(' Amount: ', chalk.white.bold(options.amount));
1114
+ console.log(' Tx Hash: ', chalk.gray(hash));
1115
+ console.log();
1116
+ } catch (err) {
1117
+ error(`Failed to approve spender: ${err}`);
1118
+ process.exit(1);
1119
+ }
1120
+ });
1121
+
1122
+ // Set integration
1123
+ cmd
1124
+ .command('integrate')
1125
+ .description('Set integration contract')
1126
+ .requiredOption('--name <name>', 'Integration name (spendingMandate|policyRegistry|crossRailLedger)')
1127
+ .requiredOption('--address <addr>', 'Integration contract address')
1128
+ .option('--json', 'Output as JSON')
1129
+ .action(async (options: {
1130
+ name: string;
1131
+ address: string;
1132
+ } & OutputOptions) => {
1133
+ try {
1134
+ const validNames = ['spendingMandate', 'policyRegistry', 'crossRailLedger'];
1135
+ if (!validNames.includes(options.name)) {
1136
+ throw new Error(`Invalid integration name. Must be one of: ${validNames.join(', ')}`);
1137
+ }
1138
+
1139
+ const integrationAddress = options.address as Address;
1140
+ const treasuryAddress = await getTreasuryAddress();
1141
+ const { client, account } = await loadWallet();
1142
+ const publicClient = getPublicClient('mainnet');
1143
+
1144
+ const hash = await withSpinner(
1145
+ 'Setting integration...',
1146
+ async () =>
1147
+ client.writeContract({
1148
+ address: treasuryAddress,
1149
+ abi: TREASURY_ABI,
1150
+ functionName: 'setIntegration',
1151
+ args: [options.name, integrationAddress],
1152
+ account,
1153
+ chain: base,
1154
+ })
1155
+ );
1156
+
1157
+ await publicClient.waitForTransactionReceipt({ hash });
1158
+
1159
+ if (outputJSON({
1160
+ name: options.name,
1161
+ address: integrationAddress,
1162
+ transactionHash: hash,
1163
+ }, options)) {
1164
+ return;
1165
+ }
1166
+
1167
+ console.log();
1168
+ success('Integration set successfully!');
1169
+ console.log();
1170
+ console.log(' Name: ', chalk.white.bold(options.name));
1171
+ console.log(' Address: ', chalk.gray(integrationAddress));
1172
+ console.log(' Tx Hash: ', chalk.gray(hash));
1173
+ console.log();
1174
+ } catch (err) {
1175
+ error(`Failed to set integration: ${err}`);
1176
+ process.exit(1);
1177
+ }
1178
+ });
1179
+
1180
+ // Withdraw ETH
1181
+ cmd
1182
+ .command('withdraw-eth')
1183
+ .description('Withdraw ETH from treasury')
1184
+ .requiredOption('--to <addr>', 'Recipient address')
1185
+ .requiredOption('--amount <amount>', 'Amount in ETH')
1186
+ .requiredOption('--reason <string>', 'Withdrawal reason')
1187
+ .option('--json', 'Output as JSON')
1188
+ .action(async (options: {
1189
+ to: string;
1190
+ amount: string;
1191
+ reason: string;
1192
+ } & OutputOptions) => {
1193
+ try {
1194
+ const toAddress = options.to as Address;
1195
+ const treasuryAddress = await getTreasuryAddress();
1196
+ const { client, account } = await loadWallet();
1197
+ const publicClient = getPublicClient('mainnet');
1198
+
1199
+ const amount = parseUnits(options.amount, 18);
1200
+
1201
+ const hash = await withSpinner(
1202
+ 'Withdrawing ETH...',
1203
+ async () =>
1204
+ client.writeContract({
1205
+ address: treasuryAddress,
1206
+ abi: TREASURY_ABI,
1207
+ functionName: 'withdrawETH',
1208
+ args: [toAddress, amount, options.reason],
1209
+ account,
1210
+ chain: base,
1211
+ })
1212
+ );
1213
+
1214
+ await publicClient.waitForTransactionReceipt({ hash });
1215
+
1216
+ if (outputJSON({
1217
+ to: toAddress,
1218
+ amount: options.amount,
1219
+ reason: options.reason,
1220
+ transactionHash: hash,
1221
+ }, options)) {
1222
+ return;
1223
+ }
1224
+
1225
+ console.log();
1226
+ success('ETH withdrawn successfully!');
1227
+ console.log();
1228
+ console.log(' Amount: ', chalk.white.bold(`${options.amount} ETH`));
1229
+ console.log(' To: ', chalk.gray(toAddress));
1230
+ console.log(' Reason: ', chalk.white(options.reason));
1231
+ console.log(' Tx Hash: ', chalk.gray(hash));
1232
+ console.log();
1233
+ } catch (err) {
1234
+ error(`Failed to withdraw ETH: ${err}`);
1235
+ process.exit(1);
1236
+ }
1237
+ });
1238
+
1239
+ // Get spend limits
1240
+ cmd
1241
+ .command('limits')
1242
+ .description('View spend limits for an operator')
1243
+ .requiredOption('--address <addr>', 'Operator address')
1244
+ .option('--json', 'Output as JSON')
1245
+ .action(async (address: string, options: OutputOptions & { address: string }) => {
1246
+ try {
1247
+ const operatorAddress = options.address as Address;
1248
+ const treasuryAddress = await getTreasuryAddress();
1249
+ const publicClient = getPublicClient('mainnet');
1250
+
1251
+ const limits = await withSpinner(
1252
+ 'Fetching spend limits...',
1253
+ async () =>
1254
+ publicClient.readContract({
1255
+ address: treasuryAddress,
1256
+ abi: TREASURY_ABI,
1257
+ functionName: 'spendLimits',
1258
+ args: [operatorAddress],
1259
+ })
1260
+ );
1261
+
1262
+ const [maxPerTx, maxPerDay, spentToday, dayStart] = limits;
1263
+
1264
+ if (outputJSON({
1265
+ operator: operatorAddress,
1266
+ maxPerTx: formatUnits(maxPerTx, 6),
1267
+ maxPerDay: formatUnits(maxPerDay, 6),
1268
+ spentToday: formatUnits(spentToday, 6),
1269
+ dayStart: Number(dayStart),
1270
+ remainingToday: formatUnits(maxPerDay - spentToday, 6),
1271
+ }, options)) {
1272
+ return;
1273
+ }
1274
+
1275
+ console.log();
1276
+ console.log(chalk.bold.cyan('šŸ’³ Spend Limits'));
1277
+ console.log();
1278
+ console.log(' Operator: ', chalk.gray(operatorAddress));
1279
+ console.log(' Max Per Tx: ', chalk.white.bold(`$${formatUnits(maxPerTx, 6)}`));
1280
+ console.log(' Max Per Day: ', chalk.white.bold(`$${formatUnits(maxPerDay, 6)}`));
1281
+ console.log(' Spent Today: ', chalk.yellow(`$${formatUnits(spentToday, 6)}`));
1282
+ console.log(' Remaining Today: ', chalk.green(`$${formatUnits(maxPerDay - spentToday, 6)}`));
1283
+ console.log(' Day Start: ', chalk.gray(new Date(Number(dayStart) * 1000).toLocaleString()));
1284
+ console.log();
1285
+ } catch (err) {
1286
+ error(`Failed to get spend limits: ${err}`);
1287
+ process.exit(1);
1288
+ }
1289
+ });
1290
+
906
1291
  return cmd;
907
1292
  }