@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.
- package/dist/src/commands/treasury.d.ts +6 -0
- package/dist/src/commands/treasury.d.ts.map +1 -0
- package/dist/src/commands/treasury.js +781 -0
- package/dist/src/commands/treasury.js.map +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/treasury.ts +907 -0
- package/src/index.ts +3 -1
|
@@ -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
|
+
}
|