@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,810 @@
1
+ import { Command } from 'commander';
2
+ import { type Address, 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, createTable, formatAddress } from '../lib/display';
7
+ import chalk from 'chalk';
8
+ import type { OutputOptions } from '../lib/types';
9
+
10
+ // InvestmentTermSheet contract
11
+ const INVESTMENT_CONTRACT = '0xfa4d9933422401e8b0846f14889b383e068860eb' as Address;
12
+
13
+ enum ReturnType {
14
+ REVENUE_SHARE = 0,
15
+ FIXED_RETURN = 1,
16
+ MILESTONE_BASED = 2,
17
+ STREAMING = 3,
18
+ }
19
+
20
+ enum InvestmentStatus {
21
+ PROPOSED = 0,
22
+ FUNDED = 1,
23
+ ACTIVE = 2,
24
+ COMPLETED = 3,
25
+ DEFAULTED = 4,
26
+ CANCELLED = 5,
27
+ }
28
+
29
+ const RETURN_TYPE_NAMES: Record<number, string> = {
30
+ 0: 'Revenue Share',
31
+ 1: 'Fixed Return',
32
+ 2: 'Milestone Based',
33
+ 3: 'Streaming',
34
+ };
35
+
36
+ const STATUS_NAMES: Record<number, string> = {
37
+ 0: 'Proposed',
38
+ 1: 'Funded',
39
+ 2: 'Active',
40
+ 3: 'Completed',
41
+ 4: 'Defaulted',
42
+ 5: 'Cancelled',
43
+ };
44
+
45
+ // ABI
46
+ const INVESTMENT_ABI = [
47
+ {
48
+ type: 'function',
49
+ name: 'propose',
50
+ stateMutability: 'nonpayable',
51
+ inputs: [
52
+ { name: '_treasury', type: 'address' },
53
+ { name: '_token', type: 'address' },
54
+ { name: '_amount', type: 'uint256' },
55
+ {
56
+ name: '_terms',
57
+ type: 'tuple',
58
+ components: [
59
+ { name: 'returnType', type: 'uint8' },
60
+ { name: 'revShareBps', type: 'uint256' },
61
+ { name: 'fixedReturnBps', type: 'uint256' },
62
+ { name: 'durationSeconds', type: 'uint256' },
63
+ { name: 'cliffSeconds', type: 'uint256' },
64
+ { name: 'milestoneCount', type: 'uint256' },
65
+ ],
66
+ },
67
+ {
68
+ name: '_protections',
69
+ type: 'tuple',
70
+ components: [
71
+ { name: 'liquidationPreference', type: 'bool' },
72
+ { name: 'maxInvestorsBps', type: 'uint256' },
73
+ { name: 'minReserveBps', type: 'uint256' },
74
+ { name: 'minReputationScore', type: 'uint256' },
75
+ ],
76
+ },
77
+ ],
78
+ outputs: [{ name: '', type: 'uint256' }],
79
+ },
80
+ {
81
+ type: 'function',
82
+ name: 'fund',
83
+ stateMutability: 'nonpayable',
84
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
85
+ outputs: [],
86
+ },
87
+ {
88
+ type: 'function',
89
+ name: 'claimReturn',
90
+ stateMutability: 'nonpayable',
91
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
92
+ outputs: [],
93
+ },
94
+ {
95
+ type: 'function',
96
+ name: 'completeMilestone',
97
+ stateMutability: 'nonpayable',
98
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
99
+ outputs: [],
100
+ },
101
+ {
102
+ type: 'function',
103
+ name: 'triggerDefault',
104
+ stateMutability: 'nonpayable',
105
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
106
+ outputs: [],
107
+ },
108
+ {
109
+ type: 'function',
110
+ name: 'cancel',
111
+ stateMutability: 'nonpayable',
112
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
113
+ outputs: [],
114
+ },
115
+ {
116
+ type: 'function',
117
+ name: 'getInvestment',
118
+ stateMutability: 'view',
119
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
120
+ outputs: [
121
+ { name: 'investor', type: 'address' },
122
+ { name: 'treasury', type: 'address' },
123
+ { name: 'token', type: 'address' },
124
+ { name: 'investedAmount', type: 'uint256' },
125
+ { name: 'returnedAmount', type: 'uint256' },
126
+ { name: 'targetReturn', type: 'uint256' },
127
+ { name: 'status', type: 'uint8' },
128
+ { name: 'createdAt', type: 'uint256' },
129
+ { name: 'fundedAt', type: 'uint256' },
130
+ { name: 'returnType', type: 'uint8' },
131
+ { name: 'durationSeconds', type: 'uint256' },
132
+ { name: 'milestonesCompleted', type: 'uint256' },
133
+ { name: 'milestoneCount', type: 'uint256' },
134
+ ],
135
+ },
136
+ {
137
+ type: 'function',
138
+ name: 'getTreasuryInvestments',
139
+ stateMutability: 'view',
140
+ inputs: [{ name: 'treasury', type: 'address' }],
141
+ outputs: [{ name: '', type: 'uint256[]' }],
142
+ },
143
+ {
144
+ type: 'function',
145
+ name: 'getInvestorPortfolio',
146
+ stateMutability: 'view',
147
+ inputs: [{ name: 'investor', type: 'address' }],
148
+ outputs: [{ name: '', type: 'uint256[]' }],
149
+ },
150
+ {
151
+ type: 'function',
152
+ name: 'getClaimable',
153
+ stateMutability: 'view',
154
+ inputs: [{ name: 'investmentId', type: 'uint256' }],
155
+ outputs: [{ name: '', type: 'uint256' }],
156
+ },
157
+ {
158
+ type: 'function',
159
+ name: 'getStats',
160
+ stateMutability: 'view',
161
+ inputs: [],
162
+ outputs: [
163
+ { name: '_totalInvestments', type: 'uint256' },
164
+ { name: '_totalInvested', type: 'uint256' },
165
+ { name: '_totalReturned', type: 'uint256' },
166
+ { name: '_totalProtocolFees', type: 'uint256' },
167
+ ],
168
+ },
169
+ {
170
+ type: 'event',
171
+ name: 'InvestmentProposed',
172
+ inputs: [
173
+ { indexed: true, name: 'investmentId', type: 'uint256' },
174
+ { indexed: true, name: 'investor', type: 'address' },
175
+ { indexed: true, name: 'treasury', type: 'address' },
176
+ { indexed: false, name: 'amount', type: 'uint256' },
177
+ ],
178
+ },
179
+ ] as const;
180
+
181
+ const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as Address;
182
+ const USDC_ABI = [
183
+ {
184
+ type: 'function',
185
+ name: 'approve',
186
+ inputs: [
187
+ { name: 'spender', type: 'address' },
188
+ { name: 'amount', type: 'uint256' },
189
+ ],
190
+ outputs: [{ name: '', type: 'bool' }],
191
+ stateMutability: 'nonpayable',
192
+ },
193
+ ] as const;
194
+
195
+ function parseReturnType(type: string): ReturnType {
196
+ const lowerType = type.toLowerCase();
197
+ switch (lowerType) {
198
+ case 'revenue-share':
199
+ return ReturnType.REVENUE_SHARE;
200
+ case 'fixed':
201
+ return ReturnType.FIXED_RETURN;
202
+ case 'milestone':
203
+ return ReturnType.MILESTONE_BASED;
204
+ case 'streaming':
205
+ return ReturnType.STREAMING;
206
+ default:
207
+ throw new Error(`Invalid return type: ${type}. Use: revenue-share, fixed, milestone, or streaming`);
208
+ }
209
+ }
210
+
211
+ export function registerInvestCommand(program: Command) {
212
+ const invest = program
213
+ .command('invest')
214
+ .description('Investment term sheet operations');
215
+
216
+ // plob invest propose
217
+ invest
218
+ .command('propose')
219
+ .description('Propose an investment to a treasury')
220
+ .requiredOption('--treasury <address>', 'Treasury address')
221
+ .requiredOption('--amount <number>', 'Investment amount in USDC')
222
+ .requiredOption('--type <type>', 'Return type: revenue-share, fixed, milestone, or streaming')
223
+ .requiredOption('--duration <days>', 'Investment duration in days')
224
+ .option('--share <bps>', 'Revenue share in basis points (e.g., 1000 = 10%)', '0')
225
+ .option('--fixed-return <bps>', 'Fixed return in basis points', '0')
226
+ .option('--milestones <count>', 'Number of milestones', '0')
227
+ .option('--cliff <days>', 'Cliff period in days', '0')
228
+ .option('--token <address>', 'Token address (default: USDC)', USDC_ADDRESS)
229
+ .option('--liquidation-pref', 'Enable liquidation preference', false)
230
+ .option('--max-investors <bps>', 'Max investors percentage (basis points)', '5000')
231
+ .option('--min-reserve <bps>', 'Min reserve percentage (basis points)', '2000')
232
+ .option('--min-reputation <score>', 'Min reputation score', '0')
233
+ .option('--json', 'Output as JSON')
234
+ .action(async (options: OutputOptions & {
235
+ treasury: string;
236
+ amount: string;
237
+ type: string;
238
+ duration: string;
239
+ share: string;
240
+ fixedReturn: string;
241
+ milestones: string;
242
+ cliff: string;
243
+ token: string;
244
+ liquidationPref: boolean;
245
+ maxInvestors: string;
246
+ minReserve: string;
247
+ minReputation: string;
248
+ }) => {
249
+ try {
250
+ const { client, account } = await loadWallet();
251
+ const publicClient = getPublicClient();
252
+
253
+ const returnType = parseReturnType(options.type);
254
+ const amountBigInt = parseUnits(options.amount, 6); // USDC has 6 decimals
255
+ const durationSeconds = BigInt(parseInt(options.duration) * 86400);
256
+ const cliffSeconds = BigInt(parseInt(options.cliff) * 86400);
257
+
258
+ const terms = {
259
+ returnType,
260
+ revShareBps: BigInt(parseInt(options.share)),
261
+ fixedReturnBps: BigInt(parseInt(options.fixedReturn)),
262
+ durationSeconds,
263
+ cliffSeconds,
264
+ milestoneCount: BigInt(parseInt(options.milestones)),
265
+ };
266
+
267
+ const protections = {
268
+ liquidationPreference: options.liquidationPref,
269
+ maxInvestorsBps: BigInt(parseInt(options.maxInvestors)),
270
+ minReserveBps: BigInt(parseInt(options.minReserve)),
271
+ minReputationScore: BigInt(parseInt(options.minReputation)),
272
+ };
273
+
274
+ // Approve USDC first
275
+ info('Approving USDC spend...');
276
+ const approveHash = await client.writeContract({
277
+ address: USDC_ADDRESS,
278
+ abi: USDC_ABI,
279
+ functionName: 'approve',
280
+ args: [INVESTMENT_CONTRACT, amountBigInt],
281
+ account,
282
+ chain: base,
283
+ });
284
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
285
+
286
+ const hash = await withSpinner('Proposing investment', async () => {
287
+ return await client.writeContract({
288
+ address: INVESTMENT_CONTRACT,
289
+ abi: INVESTMENT_ABI,
290
+ functionName: 'propose',
291
+ args: [options.treasury as Address, options.token as Address, amountBigInt, terms, protections],
292
+ account,
293
+ chain: base,
294
+ });
295
+ });
296
+
297
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
298
+
299
+ // Parse event to get investment ID
300
+ const proposedEvent = receipt.logs
301
+ .map((log) => {
302
+ try {
303
+ return {
304
+ ...log,
305
+ decoded: decodeEventLog({
306
+ abi: INVESTMENT_ABI,
307
+ data: log.data,
308
+ topics: log.topics,
309
+ }),
310
+ };
311
+ } catch {
312
+ return null;
313
+ }
314
+ })
315
+ .find((log) => log?.decoded?.eventName === 'InvestmentProposed');
316
+
317
+ const investmentId = proposedEvent?.decoded?.args?.investmentId?.toString() || 'unknown';
318
+
319
+ if (options.json) {
320
+ outputJSON({
321
+ investmentId,
322
+ transactionHash: hash,
323
+ blockNumber: receipt.blockNumber.toString(),
324
+ });
325
+ } else {
326
+ success(`Investment proposed!`);
327
+ info(`Investment ID: ${chalk.cyan(investmentId)}`);
328
+ info(`Transaction: ${chalk.gray(hash)}`);
329
+ }
330
+ } catch (err) {
331
+ error(err instanceof Error ? err.message : 'Failed to propose investment');
332
+ process.exit(1);
333
+ }
334
+ });
335
+
336
+ // plob invest fund
337
+ invest
338
+ .command('fund')
339
+ .description('Fund a proposed investment')
340
+ .argument('<id>', 'Investment ID')
341
+ .option('--json', 'Output as JSON')
342
+ .action(async (id: string, options: OutputOptions) => {
343
+ try {
344
+ const { client, account } = await loadWallet();
345
+ const publicClient = getPublicClient();
346
+
347
+ const hash = await withSpinner('Funding investment', async () => {
348
+ return await client.writeContract({
349
+ address: INVESTMENT_CONTRACT,
350
+ abi: INVESTMENT_ABI,
351
+ functionName: 'fund',
352
+ args: [BigInt(id)],
353
+ account,
354
+ chain: base,
355
+ });
356
+ });
357
+
358
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
359
+
360
+ if (options.json) {
361
+ outputJSON({
362
+ investmentId: id,
363
+ transactionHash: hash,
364
+ blockNumber: receipt.blockNumber.toString(),
365
+ });
366
+ } else {
367
+ success('Investment funded!');
368
+ info(`Transaction: ${chalk.gray(hash)}`);
369
+ }
370
+ } catch (err) {
371
+ error(err instanceof Error ? err.message : 'Failed to fund investment');
372
+ process.exit(1);
373
+ }
374
+ });
375
+
376
+ // plob invest claim
377
+ invest
378
+ .command('claim')
379
+ .description('Claim investment returns')
380
+ .argument('<id>', 'Investment ID')
381
+ .option('--json', 'Output as JSON')
382
+ .action(async (id: string, options: OutputOptions) => {
383
+ try {
384
+ const { client, account } = await loadWallet();
385
+ const publicClient = getPublicClient();
386
+
387
+ // Get claimable amount first
388
+ const claimable = await publicClient.readContract({
389
+ address: INVESTMENT_CONTRACT,
390
+ abi: INVESTMENT_ABI,
391
+ functionName: 'getClaimable',
392
+ args: [BigInt(id)],
393
+ }) as bigint;
394
+
395
+ const hash = await withSpinner('Claiming returns', async () => {
396
+ return await client.writeContract({
397
+ address: INVESTMENT_CONTRACT,
398
+ abi: INVESTMENT_ABI,
399
+ functionName: 'claimReturn',
400
+ args: [BigInt(id)],
401
+ account,
402
+ chain: base,
403
+ });
404
+ });
405
+
406
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
407
+
408
+ if (options.json) {
409
+ outputJSON({
410
+ investmentId: id,
411
+ amount: claimable.toString(),
412
+ transactionHash: hash,
413
+ blockNumber: receipt.blockNumber.toString(),
414
+ });
415
+ } else {
416
+ success(`Claimed ${chalk.green(parseFloat((Number(claimable) / 1e6).toFixed(2)).toString())} USDC`);
417
+ info(`Transaction: ${chalk.gray(hash)}`);
418
+ }
419
+ } catch (err) {
420
+ error(err instanceof Error ? err.message : 'Failed to claim returns');
421
+ process.exit(1);
422
+ }
423
+ });
424
+
425
+ // plob invest milestone
426
+ invest
427
+ .command('milestone')
428
+ .description('Complete a milestone (oracle only)')
429
+ .argument('<id>', 'Investment ID')
430
+ .option('--json', 'Output as JSON')
431
+ .action(async (id: string, options: OutputOptions) => {
432
+ try {
433
+ const { client, account } = await loadWallet();
434
+ const publicClient = getPublicClient();
435
+
436
+ const hash = await withSpinner('Completing milestone', async () => {
437
+ return await client.writeContract({
438
+ address: INVESTMENT_CONTRACT,
439
+ abi: INVESTMENT_ABI,
440
+ functionName: 'completeMilestone',
441
+ args: [BigInt(id)],
442
+ account,
443
+ chain: base,
444
+ });
445
+ });
446
+
447
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
448
+
449
+ if (options.json) {
450
+ outputJSON({
451
+ investmentId: id,
452
+ transactionHash: hash,
453
+ blockNumber: receipt.blockNumber.toString(),
454
+ });
455
+ } else {
456
+ success('Milestone completed!');
457
+ info(`Transaction: ${chalk.gray(hash)}`);
458
+ }
459
+ } catch (err) {
460
+ error(err instanceof Error ? err.message : 'Failed to complete milestone');
461
+ process.exit(1);
462
+ }
463
+ });
464
+
465
+ // plob invest default
466
+ invest
467
+ .command('default')
468
+ .description('Trigger default on investment')
469
+ .argument('<id>', 'Investment ID')
470
+ .option('--json', 'Output as JSON')
471
+ .action(async (id: string, options: OutputOptions) => {
472
+ try {
473
+ const { client, account } = await loadWallet();
474
+ const publicClient = getPublicClient();
475
+
476
+ const hash = await withSpinner('Triggering default', async () => {
477
+ return await client.writeContract({
478
+ address: INVESTMENT_CONTRACT,
479
+ abi: INVESTMENT_ABI,
480
+ functionName: 'triggerDefault',
481
+ args: [BigInt(id)],
482
+ account,
483
+ chain: base,
484
+ });
485
+ });
486
+
487
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
488
+
489
+ if (options.json) {
490
+ outputJSON({
491
+ investmentId: id,
492
+ transactionHash: hash,
493
+ blockNumber: receipt.blockNumber.toString(),
494
+ });
495
+ } else {
496
+ success('Default triggered');
497
+ info(`Transaction: ${chalk.gray(hash)}`);
498
+ }
499
+ } catch (err) {
500
+ error(err instanceof Error ? err.message : 'Failed to trigger default');
501
+ process.exit(1);
502
+ }
503
+ });
504
+
505
+ // plob invest cancel
506
+ invest
507
+ .command('cancel')
508
+ .description('Cancel an unfunded investment')
509
+ .argument('<id>', 'Investment ID')
510
+ .option('--json', 'Output as JSON')
511
+ .action(async (id: string, options: OutputOptions) => {
512
+ try {
513
+ const { client, account } = await loadWallet();
514
+ const publicClient = getPublicClient();
515
+
516
+ const hash = await withSpinner('Cancelling investment', async () => {
517
+ return await client.writeContract({
518
+ address: INVESTMENT_CONTRACT,
519
+ abi: INVESTMENT_ABI,
520
+ functionName: 'cancel',
521
+ args: [BigInt(id)],
522
+ account,
523
+ chain: base,
524
+ });
525
+ });
526
+
527
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
528
+
529
+ if (options.json) {
530
+ outputJSON({
531
+ investmentId: id,
532
+ transactionHash: hash,
533
+ blockNumber: receipt.blockNumber.toString(),
534
+ });
535
+ } else {
536
+ success('Investment cancelled');
537
+ info(`Transaction: ${chalk.gray(hash)}`);
538
+ }
539
+ } catch (err) {
540
+ error(err instanceof Error ? err.message : 'Failed to cancel investment');
541
+ process.exit(1);
542
+ }
543
+ });
544
+
545
+ // plob invest info
546
+ invest
547
+ .command('info')
548
+ .description('View investment details')
549
+ .argument('<id>', 'Investment ID')
550
+ .option('--json', 'Output as JSON')
551
+ .action(async (id: string, options: OutputOptions) => {
552
+ try {
553
+ const publicClient = getPublicClient();
554
+
555
+ const result = await publicClient.readContract({
556
+ address: INVESTMENT_CONTRACT,
557
+ abi: INVESTMENT_ABI,
558
+ functionName: 'getInvestment',
559
+ args: [BigInt(id)],
560
+ }) as [Address, Address, Address, bigint, bigint, bigint, number, bigint, bigint, number, bigint, bigint, bigint];
561
+
562
+ const [
563
+ investor,
564
+ treasury,
565
+ token,
566
+ investedAmount,
567
+ returnedAmount,
568
+ targetReturn,
569
+ status,
570
+ createdAt,
571
+ fundedAt,
572
+ returnType,
573
+ durationSeconds,
574
+ milestonesCompleted,
575
+ milestoneCount,
576
+ ] = result;
577
+
578
+ if (options.json) {
579
+ outputJSON({
580
+ investmentId: id,
581
+ investor,
582
+ treasury,
583
+ token,
584
+ investedAmount: investedAmount.toString(),
585
+ returnedAmount: returnedAmount.toString(),
586
+ targetReturn: targetReturn.toString(),
587
+ status: STATUS_NAMES[status],
588
+ createdAt: Number(createdAt),
589
+ fundedAt: Number(fundedAt),
590
+ returnType: RETURN_TYPE_NAMES[returnType],
591
+ durationSeconds: Number(durationSeconds),
592
+ milestonesCompleted: Number(milestonesCompleted),
593
+ milestoneCount: Number(milestoneCount),
594
+ });
595
+ } else {
596
+ console.log(chalk.bold.cyan('\nšŸ“Š Investment Details\n'));
597
+
598
+ const table = createTable(['Property', 'Value']);
599
+ table.push(
600
+ ['ID', chalk.white(id)],
601
+ ['Status', chalk.yellow(STATUS_NAMES[status])],
602
+ ['Type', chalk.cyan(RETURN_TYPE_NAMES[returnType])],
603
+ ['Investor', formatAddress(investor)],
604
+ ['Treasury', formatAddress(treasury)],
605
+ ['Token', formatAddress(token)],
606
+ ['Invested', `${chalk.green((Number(investedAmount) / 1e6).toFixed(2))} USDC`],
607
+ ['Returned', `${chalk.green((Number(returnedAmount) / 1e6).toFixed(2))} USDC`],
608
+ ['Target Return', `${chalk.green((Number(targetReturn) / 1e6).toFixed(2))} USDC`],
609
+ ['Duration', `${Number(durationSeconds) / 86400} days`],
610
+ ['Milestones', `${Number(milestonesCompleted)} / ${Number(milestoneCount)}`],
611
+ ['Created', new Date(Number(createdAt) * 1000).toLocaleDateString()],
612
+ ['Funded', fundedAt > 0n ? new Date(Number(fundedAt) * 1000).toLocaleDateString() : 'Not funded'],
613
+ );
614
+
615
+ console.log(table.toString());
616
+ }
617
+ } catch (err) {
618
+ error(err instanceof Error ? err.message : 'Failed to get investment info');
619
+ process.exit(1);
620
+ }
621
+ });
622
+
623
+ // plob invest portfolio
624
+ invest
625
+ .command('portfolio')
626
+ .description('View investor portfolio')
627
+ .argument('[address]', 'Investor address (default: your address)')
628
+ .option('--json', 'Output as JSON')
629
+ .action(async (address: string | undefined, options: OutputOptions) => {
630
+ try {
631
+ const publicClient = getPublicClient();
632
+ const investor = (address || (await getWalletAddress())) as Address;
633
+
634
+ const investmentIds = await publicClient.readContract({
635
+ address: INVESTMENT_CONTRACT,
636
+ abi: INVESTMENT_ABI,
637
+ functionName: 'getInvestorPortfolio',
638
+ args: [investor],
639
+ }) as bigint[];
640
+
641
+ if (options.json) {
642
+ outputJSON({
643
+ investor,
644
+ count: investmentIds.length,
645
+ investments: investmentIds.map(id => id.toString()),
646
+ });
647
+ } else {
648
+ console.log(chalk.bold.cyan(`\nšŸ’¼ Portfolio for ${formatAddress(investor)}\n`));
649
+
650
+ if (investmentIds.length === 0) {
651
+ info('No investments found');
652
+ return;
653
+ }
654
+
655
+ const table = createTable(['ID', 'Status', 'Invested', 'Returned', 'Type']);
656
+
657
+ for (const investmentId of investmentIds) {
658
+ const result = await publicClient.readContract({
659
+ address: INVESTMENT_CONTRACT,
660
+ abi: INVESTMENT_ABI,
661
+ functionName: 'getInvestment',
662
+ args: [investmentId],
663
+ }) as [Address, Address, Address, bigint, bigint, bigint, number, bigint, bigint, number, bigint, bigint, bigint];
664
+
665
+ const [, , , investedAmount, returnedAmount, , status, , , returnType] = result;
666
+
667
+ table.push([
668
+ chalk.white(investmentId.toString()),
669
+ chalk.yellow(STATUS_NAMES[status]),
670
+ `${chalk.green((Number(investedAmount) / 1e6).toFixed(2))} USDC`,
671
+ `${chalk.green((Number(returnedAmount) / 1e6).toFixed(2))} USDC`,
672
+ chalk.cyan(RETURN_TYPE_NAMES[returnType]),
673
+ ]);
674
+ }
675
+
676
+ console.log(table.toString());
677
+ info(`Total investments: ${investmentIds.length}`);
678
+ }
679
+ } catch (err) {
680
+ error(err instanceof Error ? err.message : 'Failed to get portfolio');
681
+ process.exit(1);
682
+ }
683
+ });
684
+
685
+ // plob invest treasury
686
+ invest
687
+ .command('treasury')
688
+ .description('View treasury investments')
689
+ .argument('[address]', 'Treasury address (default: your treasury)')
690
+ .option('--json', 'Output as JSON')
691
+ .action(async (address: string | undefined, options: OutputOptions) => {
692
+ try {
693
+ const publicClient = getPublicClient();
694
+
695
+ let treasury: Address;
696
+ if (address) {
697
+ treasury = address as Address;
698
+ } else {
699
+ // Try to get user's treasury from TreasuryFactory
700
+ const owner = await getWalletAddress();
701
+ treasury = await publicClient.readContract({
702
+ address: '0x171a685f28546a0ebb13059184db1f808b915066' as Address,
703
+ abi: [
704
+ {
705
+ type: 'function',
706
+ name: 'treasuries',
707
+ inputs: [{ name: '', type: 'address' }],
708
+ outputs: [{ name: '', type: 'address' }],
709
+ stateMutability: 'view',
710
+ },
711
+ ] as const,
712
+ functionName: 'treasuries',
713
+ args: [owner as Address],
714
+ }) as Address;
715
+ }
716
+
717
+ const investmentIds = await publicClient.readContract({
718
+ address: INVESTMENT_CONTRACT,
719
+ abi: INVESTMENT_ABI,
720
+ functionName: 'getTreasuryInvestments',
721
+ args: [treasury],
722
+ }) as bigint[];
723
+
724
+ if (options.json) {
725
+ outputJSON({
726
+ treasury,
727
+ count: investmentIds.length,
728
+ investments: investmentIds.map(id => id.toString()),
729
+ });
730
+ } else {
731
+ console.log(chalk.bold.cyan(`\nšŸ¦ Treasury Investments: ${formatAddress(treasury)}\n`));
732
+
733
+ if (investmentIds.length === 0) {
734
+ info('No investments found');
735
+ return;
736
+ }
737
+
738
+ const table = createTable(['ID', 'Status', 'Amount', 'Investor', 'Type']);
739
+
740
+ for (const investmentId of investmentIds) {
741
+ const result = await publicClient.readContract({
742
+ address: INVESTMENT_CONTRACT,
743
+ abi: INVESTMENT_ABI,
744
+ functionName: 'getInvestment',
745
+ args: [investmentId],
746
+ }) as [Address, Address, Address, bigint, bigint, bigint, number, bigint, bigint, number, bigint, bigint, bigint];
747
+
748
+ const [investor, , , investedAmount, , , status, , , returnType] = result;
749
+
750
+ table.push([
751
+ chalk.white(investmentId.toString()),
752
+ chalk.yellow(STATUS_NAMES[status]),
753
+ `${chalk.green((Number(investedAmount) / 1e6).toFixed(2))} USDC`,
754
+ formatAddress(investor),
755
+ chalk.cyan(RETURN_TYPE_NAMES[returnType]),
756
+ ]);
757
+ }
758
+
759
+ console.log(table.toString());
760
+ info(`Total investments: ${investmentIds.length}`);
761
+ }
762
+ } catch (err) {
763
+ error(err instanceof Error ? err.message : 'Failed to get treasury investments');
764
+ process.exit(1);
765
+ }
766
+ });
767
+
768
+ // plob invest stats
769
+ invest
770
+ .command('stats')
771
+ .description('View protocol-wide investment statistics')
772
+ .option('--json', 'Output as JSON')
773
+ .action(async (options: OutputOptions) => {
774
+ try {
775
+ const publicClient = getPublicClient();
776
+
777
+ const [totalInvestments, totalInvested, totalReturned, totalProtocolFees] = await publicClient.readContract({
778
+ address: INVESTMENT_CONTRACT,
779
+ abi: INVESTMENT_ABI,
780
+ functionName: 'getStats',
781
+ args: [],
782
+ }) as [bigint, bigint, bigint, bigint];
783
+
784
+ if (options.json) {
785
+ outputJSON({
786
+ totalInvestments: Number(totalInvestments),
787
+ totalInvested: totalInvested.toString(),
788
+ totalReturned: totalReturned.toString(),
789
+ totalProtocolFees: totalProtocolFees.toString(),
790
+ });
791
+ } else {
792
+ console.log(chalk.bold.cyan('\nšŸ“Š Protocol Investment Statistics\n'));
793
+
794
+ const table = createTable(['Metric', 'Value']);
795
+ table.push(
796
+ ['Total Investments', chalk.white(totalInvestments.toString())],
797
+ ['Total Invested', `${chalk.green((Number(totalInvested) / 1e6).toFixed(2))} USDC`],
798
+ ['Total Returned', `${chalk.green((Number(totalReturned) / 1e6).toFixed(2))} USDC`],
799
+ ['Protocol Fees', `${chalk.yellow((Number(totalProtocolFees) / 1e6).toFixed(2))} USDC`],
800
+ ['Avg Return Rate', `${chalk.cyan(((Number(totalReturned) / Number(totalInvested)) * 100).toFixed(2))}%`],
801
+ );
802
+
803
+ console.log(table.toString());
804
+ }
805
+ } catch (err) {
806
+ error(err instanceof Error ? err.message : 'Failed to get stats');
807
+ process.exit(1);
808
+ }
809
+ });
810
+ }