@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.
- package/README.md +82 -40
- package/dist/src/commands/dashboard.d.ts +3 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -0
- package/dist/src/commands/dashboard.js +67 -0
- package/dist/src/commands/dashboard.js.map +1 -0
- package/dist/src/commands/export.d.ts +3 -0
- package/dist/src/commands/export.d.ts.map +1 -0
- package/dist/src/commands/export.js +97 -0
- package/dist/src/commands/export.js.map +1 -0
- package/dist/src/commands/health.d.ts +3 -0
- package/dist/src/commands/health.d.ts.map +1 -0
- package/dist/src/commands/health.js +165 -0
- package/dist/src/commands/health.js.map +1 -0
- package/dist/src/commands/invest.d.ts +3 -0
- package/dist/src/commands/invest.d.ts.map +1 -0
- package/dist/src/commands/invest.js +721 -0
- package/dist/src/commands/invest.js.map +1 -0
- package/dist/src/commands/quickstart.d.ts +3 -0
- package/dist/src/commands/quickstart.d.ts.map +1 -0
- package/dist/src/commands/quickstart.js +138 -0
- package/dist/src/commands/quickstart.js.map +1 -0
- package/dist/src/commands/search.d.ts +3 -0
- package/dist/src/commands/search.d.ts.map +1 -0
- package/dist/src/commands/search.js +126 -0
- package/dist/src/commands/search.js.map +1 -0
- package/dist/src/commands/treasury.d.ts.map +1 -1
- package/dist/src/commands/treasury.js +317 -0
- package/dist/src/commands/treasury.js.map +1 -1
- package/dist/src/commands/trust-graph.d.ts +3 -0
- package/dist/src/commands/trust-graph.d.ts.map +1 -0
- package/dist/src/commands/trust-graph.js +282 -0
- package/dist/src/commands/trust-graph.js.map +1 -0
- package/dist/src/commands/whoami.d.ts +3 -0
- package/dist/src/commands/whoami.d.ts.map +1 -0
- package/dist/src/commands/whoami.js +160 -0
- package/dist/src/commands/whoami.js.map +1 -0
- package/dist/src/index.js +18 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/contracts.d.ts +177 -23
- package/dist/src/lib/contracts.d.ts.map +1 -1
- package/dist/src/lib/contracts.js +151 -0
- package/dist/src/lib/contracts.js.map +1 -1
- package/package.json +4 -2
- package/src/commands/dashboard.ts +69 -0
- package/src/commands/export.ts +132 -0
- package/src/commands/health.ts +212 -0
- package/src/commands/invest.ts +810 -0
- package/src/commands/quickstart.ts +150 -0
- package/src/commands/search.ts +151 -0
- package/src/commands/treasury.ts +385 -0
- package/src/commands/trust-graph.ts +267 -0
- package/src/commands/whoami.ts +172 -0
- package/src/index.ts +18 -2
- 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
|
+
}
|