@zemyth/raise-sdk 0.1.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 +416 -0
  2. package/dist/accounts/index.cjs +258 -0
  3. package/dist/accounts/index.cjs.map +1 -0
  4. package/dist/accounts/index.d.cts +115 -0
  5. package/dist/accounts/index.d.ts +115 -0
  6. package/dist/accounts/index.js +245 -0
  7. package/dist/accounts/index.js.map +1 -0
  8. package/dist/constants/index.cjs +174 -0
  9. package/dist/constants/index.cjs.map +1 -0
  10. package/dist/constants/index.d.cts +143 -0
  11. package/dist/constants/index.d.ts +143 -0
  12. package/dist/constants/index.js +158 -0
  13. package/dist/constants/index.js.map +1 -0
  14. package/dist/errors/index.cjs +177 -0
  15. package/dist/errors/index.cjs.map +1 -0
  16. package/dist/errors/index.d.cts +83 -0
  17. package/dist/errors/index.d.ts +83 -0
  18. package/dist/errors/index.js +170 -0
  19. package/dist/errors/index.js.map +1 -0
  20. package/dist/index.cjs +2063 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +680 -0
  23. package/dist/index.d.ts +680 -0
  24. package/dist/index.js +1926 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/instructions/index.cjs +852 -0
  27. package/dist/instructions/index.cjs.map +1 -0
  28. package/dist/instructions/index.d.cts +452 -0
  29. package/dist/instructions/index.d.ts +452 -0
  30. package/dist/instructions/index.js +809 -0
  31. package/dist/instructions/index.js.map +1 -0
  32. package/dist/pdas/index.cjs +241 -0
  33. package/dist/pdas/index.cjs.map +1 -0
  34. package/dist/pdas/index.d.cts +171 -0
  35. package/dist/pdas/index.d.ts +171 -0
  36. package/dist/pdas/index.js +217 -0
  37. package/dist/pdas/index.js.map +1 -0
  38. package/dist/types/index.cjs +44 -0
  39. package/dist/types/index.cjs.map +1 -0
  40. package/dist/types/index.d.cts +229 -0
  41. package/dist/types/index.d.ts +229 -0
  42. package/dist/types/index.js +39 -0
  43. package/dist/types/index.js.map +1 -0
  44. package/package.json +130 -0
  45. package/src/accounts/index.ts +329 -0
  46. package/src/client.ts +715 -0
  47. package/src/constants/index.ts +205 -0
  48. package/src/errors/index.ts +222 -0
  49. package/src/events/index.ts +256 -0
  50. package/src/index.ts +253 -0
  51. package/src/instructions/index.ts +1504 -0
  52. package/src/pdas/index.ts +404 -0
  53. package/src/types/index.ts +267 -0
  54. package/src/utils/index.ts +277 -0
@@ -0,0 +1,1504 @@
1
+ /**
2
+ * Raise Instruction Builders
3
+ *
4
+ * All instruction builder functions for the Raise program.
5
+ * These return transaction signatures when called with RPC.
6
+ */
7
+
8
+ import { Program, BN } from '@coral-xyz/anchor';
9
+ import { PublicKey, Keypair, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY, SystemProgram, ComputeBudgetProgram } from '@solana/web3.js';
10
+ import {
11
+ getProjectPDA,
12
+ getEscrowPDA,
13
+ getMilestonePDA,
14
+ getInvestmentPDA,
15
+ getVotePDA,
16
+ getPivotProposalPDA,
17
+ getTgeEscrowPDA,
18
+ getTokenVaultPDA,
19
+ getNftMintPDA,
20
+ getProgramAuthorityPDA,
21
+ getAdminConfigPDA,
22
+ // ZTM v2.0 PDAs
23
+ getTokenomicsPDA,
24
+ getTokenMintPDA,
25
+ getVaultAuthorityPDA,
26
+ getInvestorVaultPDA,
27
+ getFounderVaultPDA,
28
+ getLpTokenVaultPDA,
29
+ getTreasuryVaultPDA,
30
+ getLpUsdcVaultPDA,
31
+ getFounderVestingPDA,
32
+ } from '../pdas/index.js';
33
+ import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
34
+
35
+ // Metaplex Token Metadata Program ID
36
+ const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
37
+
38
+ /**
39
+ * Ensure value is a proper PublicKey instance.
40
+ * Handles cases where PublicKey objects lose their prototype chain
41
+ * (e.g., when passing through React state or JSON serialization).
42
+ */
43
+ function ensurePublicKey(value: PublicKey | string | { toString(): string }): PublicKey {
44
+ if (value instanceof PublicKey) {
45
+ return value;
46
+ }
47
+ // Handle string or object with toString method
48
+ return new PublicKey(String(value));
49
+ }
50
+
51
+ // Generic type for any Anchor program
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ type AnyProgram = Program<any>;
54
+
55
+ // Helper to get methods namespace - bypasses deep type instantiation
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ function getMethods(program: AnyProgram): any {
58
+ return program.methods;
59
+ }
60
+
61
+ // Helper to get account namespace for fetching accounts
62
+ // Used by voteOnMilestone to fetch milestone.voting_round for vote PDA
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ function getAccountNamespace(program: AnyProgram): any {
65
+ return program.account;
66
+ }
67
+
68
+ // =============================================================================
69
+ // Admin Instructions
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Initialize admin config (deploy-time only)
74
+ */
75
+ export async function initializeAdmin(
76
+ program: AnyProgram,
77
+ admin: PublicKey,
78
+ payer: PublicKey
79
+ ): Promise<string> {
80
+ return getMethods(program)
81
+ .initializeAdmin()
82
+ .accounts({
83
+ admin,
84
+ payer,
85
+ })
86
+ .rpc();
87
+ }
88
+
89
+ /**
90
+ * Propose admin transfer to new admin
91
+ */
92
+ export async function transferAdmin(
93
+ program: AnyProgram,
94
+ adminKeypair: Keypair,
95
+ newAdmin: PublicKey
96
+ ): Promise<string> {
97
+ return getMethods(program)
98
+ .transferAdmin()
99
+ .accounts({
100
+ authority: adminKeypair.publicKey,
101
+ newAdmin,
102
+ })
103
+ .signers([adminKeypair])
104
+ .rpc();
105
+ }
106
+
107
+ /**
108
+ * Accept admin transfer
109
+ */
110
+ export async function acceptAdmin(
111
+ program: AnyProgram,
112
+ newAuthority: PublicKey
113
+ ): Promise<string> {
114
+ return getMethods(program)
115
+ .acceptAdmin()
116
+ .accounts({
117
+ newAuthority,
118
+ })
119
+ .rpc();
120
+ }
121
+
122
+ // =============================================================================
123
+ // Project Instructions
124
+ // =============================================================================
125
+
126
+ /**
127
+ * TierConfig input type for initializeProject
128
+ * Matches the on-chain TierConfig struct
129
+ */
130
+ interface TierConfigInput {
131
+ /** USDC amount per lot */
132
+ amount: BN;
133
+ /** Maximum lots available */
134
+ maxLots: number;
135
+ /** Token allocation per $1 invested */
136
+ tokenRatio: BN;
137
+ /** Vote weight multiplier (basis points, 100 = 1.0x) */
138
+ voteMultiplier: number;
139
+ }
140
+
141
+ /**
142
+ * TokenomicsArgs input type for initializeProject (ZTM v2.0)
143
+ * Matches the on-chain TokenomicsArgs struct
144
+ */
145
+ export interface TokenomicsInput {
146
+ /** Token symbol as 8-byte array (2-8 chars uppercase, padded with 0s) */
147
+ tokenSymbol: number[];
148
+ /** Total token supply */
149
+ totalSupply: BN;
150
+ /** Investor allocation in basis points (e.g., 4000 = 40%) */
151
+ investorAllocationBps: number;
152
+ /** LP token allocation in basis points */
153
+ lpTokenAllocationBps: number;
154
+ /** LP USDC allocation in basis points (min 500 = 5% of raised USDC) */
155
+ lpUsdcAllocationBps: number;
156
+ /** Founder allocation in basis points (optional) */
157
+ founderAllocationBps?: number | null;
158
+ /** Treasury allocation in basis points (optional) */
159
+ treasuryAllocationBps?: number | null;
160
+ /** Founder wallet for vesting (required if founder_allocation_bps > 0) */
161
+ founderWallet?: PublicKey | null;
162
+ /** Vesting duration in months (required if founder_allocation_bps > 0) */
163
+ vestingDurationMonths?: number | null;
164
+ /** Cliff period in months (optional) */
165
+ cliffMonths?: number | null;
166
+ }
167
+
168
+ /**
169
+ * Helper to convert string symbol to 8-byte array
170
+ */
171
+ export function symbolToBytes(symbol: string): number[] {
172
+ const bytes = new Array(8).fill(0);
173
+ const chars = symbol.toUpperCase().slice(0, 8);
174
+ for (let i = 0; i < chars.length; i++) {
175
+ bytes[i] = chars.charCodeAt(i);
176
+ }
177
+ return bytes;
178
+ }
179
+
180
+ // =============================================================================
181
+ // Deadline Constants and Helpers
182
+ // =============================================================================
183
+
184
+ /** Minimum deadline duration from current time (7 days in production, 60s in dev) */
185
+ export const MIN_DEADLINE_DURATION_SECONDS_PROD = 604_800; // 7 days
186
+ export const MIN_DEADLINE_DURATION_SECONDS_DEV = 60; // 60 seconds
187
+
188
+ /** Maximum deadline duration from current time (1 year) */
189
+ export const MAX_DEADLINE_DURATION_SECONDS = 31_536_000; // 365 days
190
+
191
+ /**
192
+ * Calculate a valid milestone deadline
193
+ *
194
+ * @param daysFromNow - Number of days from now to set deadline
195
+ * @param isDev - Use dev mode (60s min) or production mode (7 days min)
196
+ * @returns BN timestamp for the deadline
197
+ */
198
+ export function calculateDeadline(daysFromNow: number, isDev: boolean = false): BN {
199
+ const nowSeconds = Math.floor(Date.now() / 1000);
200
+ const minDuration = isDev ? MIN_DEADLINE_DURATION_SECONDS_DEV : MIN_DEADLINE_DURATION_SECONDS_PROD;
201
+ const daysInSeconds = daysFromNow * 24 * 60 * 60;
202
+
203
+ // Ensure deadline is at least minimum duration from now
204
+ const deadlineSeconds = nowSeconds + Math.max(daysInSeconds, minDuration);
205
+
206
+ // Cap at maximum duration
207
+ const maxDeadline = nowSeconds + MAX_DEADLINE_DURATION_SECONDS;
208
+ return new BN(Math.min(deadlineSeconds, maxDeadline));
209
+ }
210
+
211
+ /**
212
+ * Create a deadline that's the minimum allowed duration from now
213
+ *
214
+ * @param isDev - Use dev mode (60s min) or production mode (7 days min)
215
+ * @returns BN timestamp for the minimum valid deadline
216
+ */
217
+ export function minDeadline(isDev: boolean = false): BN {
218
+ const nowSeconds = Math.floor(Date.now() / 1000);
219
+ const minDuration = isDev ? MIN_DEADLINE_DURATION_SECONDS_DEV : MIN_DEADLINE_DURATION_SECONDS_PROD;
220
+ return new BN(nowSeconds + minDuration + 1); // +1 for safety margin
221
+ }
222
+
223
+ /**
224
+ * Validate a deadline is within allowed bounds
225
+ *
226
+ * @param deadline - BN timestamp to validate
227
+ * @param isDev - Use dev mode (60s min) or production mode (7 days min)
228
+ * @returns { valid: boolean, error?: string }
229
+ */
230
+ export function validateDeadline(
231
+ deadline: BN,
232
+ isDev: boolean = false
233
+ ): { valid: boolean; error?: string } {
234
+ const nowSeconds = Math.floor(Date.now() / 1000);
235
+ const deadlineSeconds = deadline.toNumber();
236
+ const minDuration = isDev ? MIN_DEADLINE_DURATION_SECONDS_DEV : MIN_DEADLINE_DURATION_SECONDS_PROD;
237
+
238
+ const minDeadline = nowSeconds + minDuration;
239
+ const maxDeadline = nowSeconds + MAX_DEADLINE_DURATION_SECONDS;
240
+
241
+ if (deadlineSeconds < minDeadline) {
242
+ const minDays = isDev ? '60 seconds' : '7 days';
243
+ return {
244
+ valid: false,
245
+ error: `Deadline must be at least ${minDays} from now`,
246
+ };
247
+ }
248
+
249
+ if (deadlineSeconds > maxDeadline) {
250
+ return {
251
+ valid: false,
252
+ error: 'Deadline must be within 1 year from now',
253
+ };
254
+ }
255
+
256
+ return { valid: true };
257
+ }
258
+
259
+ /**
260
+ * Initialize a new project with founder-configured tiers and tokenomics (ZTM v2.0)
261
+ *
262
+ * @param milestone1Deadline - Unix timestamp for M1 deadline (required)
263
+ * Must be >= current_time + MIN_DEADLINE_DURATION_SECONDS (7 days prod, 60s dev)
264
+ * Must be <= current_time + MAX_DEADLINE_DURATION_SECONDS (1 year)
265
+ */
266
+ export async function initializeProject(
267
+ program: AnyProgram,
268
+ args: {
269
+ projectId: BN;
270
+ fundingGoal: BN;
271
+ metadataUri: string;
272
+ /** Founder-configured tiers (1-10 tiers, sorted ascending by amount) */
273
+ tiers: TierConfigInput[];
274
+ /** ZTM v2.0: Tokenomics configuration */
275
+ tokenomics: TokenomicsInput;
276
+ /** Milestone 1 deadline - Unix timestamp (required) */
277
+ milestone1Deadline: BN;
278
+ },
279
+ founder: PublicKey
280
+ ): Promise<string> {
281
+ return getMethods(program)
282
+ .initializeProject({
283
+ projectId: args.projectId,
284
+ fundingGoal: args.fundingGoal,
285
+ metadataUri: args.metadataUri,
286
+ tiers: args.tiers,
287
+ tokenomics: {
288
+ tokenSymbol: args.tokenomics.tokenSymbol,
289
+ totalSupply: args.tokenomics.totalSupply,
290
+ investorAllocationBps: args.tokenomics.investorAllocationBps,
291
+ lpTokenAllocationBps: args.tokenomics.lpTokenAllocationBps,
292
+ lpUsdcAllocationBps: args.tokenomics.lpUsdcAllocationBps,
293
+ founderAllocationBps: args.tokenomics.founderAllocationBps ?? null,
294
+ treasuryAllocationBps: args.tokenomics.treasuryAllocationBps ?? null,
295
+ founderWallet: args.tokenomics.founderWallet ?? null,
296
+ vestingDurationMonths: args.tokenomics.vestingDurationMonths ?? null,
297
+ cliffMonths: args.tokenomics.cliffMonths ?? null,
298
+ },
299
+ milestone1Deadline: args.milestone1Deadline,
300
+ })
301
+ .accounts({
302
+ founder,
303
+ })
304
+ .rpc();
305
+ }
306
+
307
+ /**
308
+ * Submit project for approval
309
+ */
310
+ export async function submitForApproval(
311
+ program: AnyProgram,
312
+ projectId: BN,
313
+ founder: PublicKey
314
+ ): Promise<string> {
315
+ const projectPda = getProjectPDA(projectId, program.programId);
316
+
317
+ return getMethods(program)
318
+ .submitForApproval()
319
+ .accounts({
320
+ project: projectPda,
321
+ founder,
322
+ })
323
+ .rpc();
324
+ }
325
+
326
+ /**
327
+ * Approve project (admin only)
328
+ * ZTM v2.0: This now deploys the token and creates all vaults
329
+ */
330
+ export async function approveProject(
331
+ program: AnyProgram,
332
+ args: {
333
+ projectId: BN;
334
+ /** USDC mint address (for creating lp_usdc_vault) */
335
+ usdcMint: PublicKey;
336
+ },
337
+ adminKeypair: Keypair
338
+ ): Promise<string> {
339
+ const projectPda = getProjectPDA(args.projectId, program.programId);
340
+ const tokenomicsPda = getTokenomicsPDA(projectPda, program.programId);
341
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
342
+ const tokenMintPda = getTokenMintPDA(projectPda, program.programId);
343
+ const vaultAuthorityPda = getVaultAuthorityPDA(projectPda, program.programId);
344
+ const investorVaultPda = getInvestorVaultPDA(projectPda, program.programId);
345
+ const founderVaultPda = getFounderVaultPDA(projectPda, program.programId);
346
+ const lpTokenVaultPda = getLpTokenVaultPDA(projectPda, program.programId);
347
+ const treasuryVaultPda = getTreasuryVaultPDA(projectPda, program.programId);
348
+ const lpUsdcVaultPda = getLpUsdcVaultPDA(projectPda, program.programId);
349
+
350
+ return getMethods(program)
351
+ .approveProject()
352
+ .accounts({
353
+ project: projectPda,
354
+ tokenomics: tokenomicsPda,
355
+ tokenVault: tokenVaultPda,
356
+ tokenMint: tokenMintPda,
357
+ vaultAuthority: vaultAuthorityPda,
358
+ investorVault: investorVaultPda,
359
+ founderVault: founderVaultPda,
360
+ lpTokenVault: lpTokenVaultPda,
361
+ treasuryVault: treasuryVaultPda,
362
+ lpUsdcVault: lpUsdcVaultPda,
363
+ usdcMint: args.usdcMint,
364
+ authority: adminKeypair.publicKey,
365
+ payer: adminKeypair.publicKey,
366
+ })
367
+ .signers([adminKeypair])
368
+ .rpc();
369
+ }
370
+
371
+ // =============================================================================
372
+ // Milestone Instructions
373
+ // =============================================================================
374
+
375
+ /**
376
+ * Create a milestone for a project
377
+ */
378
+ export async function createMilestone(
379
+ program: AnyProgram,
380
+ args: {
381
+ projectId: BN;
382
+ milestoneIndex: number;
383
+ percentage: number;
384
+ description: string;
385
+ },
386
+ founder: PublicKey
387
+ ): Promise<string> {
388
+ const projectPda = getProjectPDA(args.projectId, program.programId);
389
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
390
+
391
+ return getMethods(program)
392
+ .createMilestone({
393
+ milestoneIndex: args.milestoneIndex,
394
+ percentage: args.percentage,
395
+ description: args.description,
396
+ })
397
+ .accounts({
398
+ project: projectPda,
399
+ milestone: milestonePda,
400
+ founder,
401
+ })
402
+ .rpc();
403
+ }
404
+
405
+ /**
406
+ * Submit milestone for review
407
+ */
408
+ export async function submitMilestone(
409
+ program: AnyProgram,
410
+ projectId: BN,
411
+ milestoneIndex: number,
412
+ founder: PublicKey
413
+ ): Promise<string> {
414
+ const projectPda = getProjectPDA(projectId, program.programId);
415
+ const milestonePda = getMilestonePDA(projectPda, milestoneIndex, program.programId);
416
+
417
+ return getMethods(program)
418
+ .submitMilestone()
419
+ .accounts({
420
+ project: projectPda,
421
+ milestone: milestonePda,
422
+ founder,
423
+ })
424
+ .rpc();
425
+ }
426
+
427
+ /**
428
+ * Vote on a milestone
429
+ *
430
+ * Automatically fetches the milestone to get the current voting_round
431
+ * for proper vote PDA derivation. This supports re-voting after milestone failure.
432
+ */
433
+ export async function voteOnMilestone(
434
+ program: AnyProgram,
435
+ args: {
436
+ projectId: BN;
437
+ milestoneIndex: number;
438
+ nftMint: PublicKey | string;
439
+ choice: { good: object } | { bad: object };
440
+ },
441
+ voter: PublicKey
442
+ ): Promise<string> {
443
+ // Ensure nftMint is a proper PublicKey (handles React state serialization)
444
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
445
+
446
+ const projectPda = getProjectPDA(args.projectId, program.programId);
447
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
448
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
449
+
450
+ // Fetch milestone to get current voting_round for vote PDA derivation
451
+ // This enables re-voting after milestone failure and resubmit
452
+ const milestone = await getAccountNamespace(program).milestone.fetch(milestonePda);
453
+ const votingRound = milestone.votingRound ?? 0;
454
+ const votePda = getVotePDA(milestonePda, voter, votingRound, program.programId);
455
+
456
+ // Get voter's NFT token account (ATA)
457
+ const voterNftAccount = getAssociatedTokenAddressSync(
458
+ nftMintPubkey,
459
+ voter,
460
+ false, // allowOwnerOffCurve
461
+ TOKEN_PROGRAM_ID
462
+ );
463
+
464
+ return getMethods(program)
465
+ .voteOnMilestone({ choice: args.choice })
466
+ .accounts({
467
+ milestone: milestonePda,
468
+ project: projectPda,
469
+ investment: investmentPda,
470
+ vote: votePda,
471
+ nftMint: nftMintPubkey,
472
+ voterNftAccount,
473
+ voter,
474
+ })
475
+ .rpc();
476
+ }
477
+
478
+ /**
479
+ * Finalize voting on a milestone
480
+ */
481
+ export async function finalizeVoting(
482
+ program: AnyProgram,
483
+ projectId: BN,
484
+ milestoneIndex: number
485
+ ): Promise<string> {
486
+ const projectPda = getProjectPDA(projectId, program.programId);
487
+ const milestonePda = getMilestonePDA(projectPda, milestoneIndex, program.programId);
488
+
489
+ return getMethods(program)
490
+ .finalizeVoting()
491
+ .accounts({
492
+ project: projectPda,
493
+ milestone: milestonePda,
494
+ })
495
+ .rpc();
496
+ }
497
+
498
+ /**
499
+ * Claim milestone funds (for founders)
500
+ *
501
+ * ZTM v2.0: Transfers USDC from escrow to founder's account.
502
+ * - Regular milestones: Full payout to founder (no LP deduction)
503
+ * - Final milestone: LP USDC reserved for PCL, triggers MAE
504
+ *
505
+ * @param nextMilestoneDeadline - Deadline for next milestone (required for non-final milestones)
506
+ * Must be >= current_time + MIN_DEADLINE_DURATION_SECONDS (7 days prod, 60s dev)
507
+ * Must be <= current_time + MAX_DEADLINE_DURATION_SECONDS (1 year)
508
+ * Set to BN(0) for final milestone claims (no next milestone exists)
509
+ */
510
+ export async function claimMilestoneFunds(
511
+ program: AnyProgram,
512
+ args: {
513
+ projectId: BN;
514
+ milestoneIndex: number;
515
+ founderUsdcAccount: PublicKey;
516
+ escrowTokenAccount: PublicKey;
517
+ /** Deadline for next milestone - required for non-final milestones, use BN(0) for final */
518
+ nextMilestoneDeadline: BN;
519
+ /** Next milestone PDA - required for non-final milestones */
520
+ nextMilestonePda?: PublicKey;
521
+ },
522
+ founder: PublicKey
523
+ ): Promise<string> {
524
+ const projectPda = getProjectPDA(args.projectId, program.programId);
525
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
526
+ const escrowPda = getEscrowPDA(args.projectId, program.programId);
527
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
528
+ const tokenomicsPda = getTokenomicsPDA(projectPda, program.programId);
529
+ const lpUsdcVaultPda = getLpUsdcVaultPDA(projectPda, program.programId);
530
+
531
+ // For non-final milestones, derive next milestone PDA if not provided
532
+ const nextMilestonePda = args.nextMilestonePda ??
533
+ (args.nextMilestoneDeadline.gt(new BN(0))
534
+ ? getMilestonePDA(projectPda, args.milestoneIndex + 1, program.programId)
535
+ : null);
536
+
537
+ return getMethods(program)
538
+ .claimMilestoneFunds({ nextMilestoneDeadline: args.nextMilestoneDeadline })
539
+ .accounts({
540
+ milestone: milestonePda,
541
+ project: projectPda,
542
+ founder,
543
+ projectEscrow: args.escrowTokenAccount,
544
+ founderUsdcAccount: args.founderUsdcAccount,
545
+ escrowPda,
546
+ tokenVault: tokenVaultPda,
547
+ tokenomics: tokenomicsPda,
548
+ lpUsdcVault: lpUsdcVaultPda,
549
+ nextMilestone: nextMilestonePda,
550
+ systemProgram: SystemProgram.programId,
551
+ tokenProgram: TOKEN_PROGRAM_ID,
552
+ })
553
+ .rpc();
554
+ }
555
+
556
+ /**
557
+ * Resubmit a failed milestone for rework (Failed → InProgress)
558
+ *
559
+ * This allows founders to iterate on a failed milestone by transitioning it
560
+ * back to InProgress state with cleared voting state for a fresh voting cycle.
561
+ * The consecutive_failures counter is NOT reset (tracked at project level).
562
+ * Unlimited rework attempts are allowed.
563
+ */
564
+ export async function resubmitMilestone(
565
+ program: AnyProgram,
566
+ args: {
567
+ projectId: BN;
568
+ milestoneIndex: number;
569
+ },
570
+ founder: PublicKey
571
+ ): Promise<string> {
572
+ const projectPda = getProjectPDA(args.projectId, program.programId);
573
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
574
+
575
+ return getMethods(program)
576
+ .resubmitMilestone()
577
+ .accounts({
578
+ project: projectPda,
579
+ milestone: milestonePda,
580
+ founder,
581
+ })
582
+ .rpc();
583
+ }
584
+
585
+ /**
586
+ * Set milestone deadline for founder to commit submission date
587
+ *
588
+ * Founders must set deadlines for milestones to provide visibility to investors.
589
+ * Deadline must be at least 7 days from now and at most 1 year from now.
590
+ * Can only be set on Proposed, Approved, or InProgress milestones.
591
+ */
592
+ export async function setMilestoneDeadline(
593
+ program: AnyProgram,
594
+ args: {
595
+ projectId: BN;
596
+ milestoneIndex: number;
597
+ /** Unix timestamp for the deadline */
598
+ deadline: BN;
599
+ },
600
+ founder: PublicKey
601
+ ): Promise<string> {
602
+ const projectPda = getProjectPDA(args.projectId, program.programId);
603
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
604
+
605
+ return getMethods(program)
606
+ .setMilestoneDeadline({
607
+ milestoneIndex: args.milestoneIndex,
608
+ deadline: args.deadline,
609
+ })
610
+ .accounts({
611
+ project: projectPda,
612
+ milestone: milestonePda,
613
+ founder,
614
+ })
615
+ .rpc();
616
+ }
617
+
618
+ /**
619
+ * Extend milestone deadline (max 3 extensions per milestone)
620
+ *
621
+ * Founders can extend a deadline up to 3 times before it passes.
622
+ * Must be called BEFORE the current deadline passes.
623
+ * New deadline must be later than current deadline.
624
+ */
625
+ export async function extendMilestoneDeadline(
626
+ program: AnyProgram,
627
+ args: {
628
+ projectId: BN;
629
+ milestoneIndex: number;
630
+ /** New deadline timestamp (must be > current deadline) */
631
+ newDeadline: BN;
632
+ },
633
+ founder: PublicKey
634
+ ): Promise<string> {
635
+ const projectPda = getProjectPDA(args.projectId, program.programId);
636
+ const milestonePda = getMilestonePDA(projectPda, args.milestoneIndex, program.programId);
637
+
638
+ return getMethods(program)
639
+ .extendMilestoneDeadline({
640
+ milestoneIndex: args.milestoneIndex,
641
+ newDeadline: args.newDeadline,
642
+ })
643
+ .accounts({
644
+ project: projectPda,
645
+ milestone: milestonePda,
646
+ founder,
647
+ })
648
+ .rpc();
649
+ }
650
+
651
+ // =============================================================================
652
+ // Investment Instructions
653
+ // =============================================================================
654
+
655
+ /**
656
+ * Derive Metaplex metadata PDA
657
+ */
658
+ function getMetadataPDA(mint: PublicKey): PublicKey {
659
+ const [pda] = PublicKey.findProgramAddressSync(
660
+ [
661
+ Buffer.from('metadata'),
662
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
663
+ mint.toBuffer(),
664
+ ],
665
+ TOKEN_METADATA_PROGRAM_ID
666
+ );
667
+ return pda;
668
+ }
669
+
670
+ /**
671
+ * Derive Metaplex master edition PDA
672
+ */
673
+ function getMasterEditionPDA(mint: PublicKey): PublicKey {
674
+ const [pda] = PublicKey.findProgramAddressSync(
675
+ [
676
+ Buffer.from('metadata'),
677
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
678
+ mint.toBuffer(),
679
+ Buffer.from('edition'),
680
+ ],
681
+ TOKEN_METADATA_PROGRAM_ID
682
+ );
683
+ return pda;
684
+ }
685
+
686
+ /**
687
+ * Invest in a project
688
+ *
689
+ * This creates an investment NFT and transfers USDC to the project escrow.
690
+ * The investmentCount should be fetched from the project account before calling.
691
+ */
692
+ export async function invest(
693
+ program: AnyProgram,
694
+ args: {
695
+ projectId: BN;
696
+ amount: BN;
697
+ investorTokenAccount: PublicKey;
698
+ escrowTokenAccount: PublicKey;
699
+ investmentCount: number; // Must be fetched from project.investmentCount
700
+ },
701
+ investor: PublicKey
702
+ ): Promise<string> {
703
+ const projectPda = getProjectPDA(args.projectId, program.programId);
704
+
705
+ // Derive NFT mint PDA using seeds: [NFT_MINT_SEED, project_id, investor, investment_count]
706
+ const [nftMint] = getNftMintPDA(args.projectId, investor, args.investmentCount, program.programId);
707
+
708
+ // Derive investment PDA using seeds: [INVESTMENT_SEED, project, nft_mint]
709
+ const investmentPda = getInvestmentPDA(projectPda, nftMint, program.programId);
710
+
711
+ // Derive investor's NFT token account (ATA)
712
+ const investorNftAccount = getAssociatedTokenAddressSync(nftMint, investor);
713
+
714
+ // Derive Metaplex metadata and master edition PDAs
715
+ const metadataAccount = getMetadataPDA(nftMint);
716
+ const masterEdition = getMasterEditionPDA(nftMint);
717
+
718
+ // Derive program authority PDA
719
+ const [programAuthority] = getProgramAuthorityPDA(program.programId);
720
+
721
+ // BUG-1 FIX: Derive first milestone PDA (index 0) for state transition when funded
722
+ const firstMilestonePda = getMilestonePDA(projectPda, 0, program.programId);
723
+
724
+ // Add compute budget instruction to handle heavy NFT+metadata operations
725
+ // Metaplex NFT minting requires significantly more than the default 200k CU
726
+ return getMethods(program)
727
+ .invest({ amount: args.amount })
728
+ .accounts({
729
+ project: projectPda,
730
+ firstMilestone: firstMilestonePda,
731
+ nftMint: nftMint,
732
+ investment: investmentPda,
733
+ investorNftAccount: investorNftAccount,
734
+ metadataAccount: metadataAccount,
735
+ masterEdition: masterEdition,
736
+ escrowTokenAccount: args.escrowTokenAccount,
737
+ investorTokenAccount: args.investorTokenAccount,
738
+ programAuthority: programAuthority,
739
+ investor,
740
+ tokenProgram: TOKEN_PROGRAM_ID,
741
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
742
+ systemProgram: SystemProgram.programId,
743
+ rent: SYSVAR_RENT_PUBKEY,
744
+ tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
745
+ sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY,
746
+ })
747
+ .preInstructions([
748
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
749
+ ])
750
+ .rpc();
751
+ }
752
+
753
+ /**
754
+ * Cancel investment within 24-hour cooling-off period
755
+ */
756
+ export async function cancelInvestment(
757
+ program: AnyProgram,
758
+ args: {
759
+ projectId: BN;
760
+ nftMint: PublicKey;
761
+ investorNftAccount: PublicKey;
762
+ investorUsdcAccount: PublicKey;
763
+ escrowTokenAccount: PublicKey;
764
+ },
765
+ investor: PublicKey
766
+ ): Promise<string> {
767
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
768
+ const projectPda = getProjectPDA(args.projectId, program.programId);
769
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
770
+ const escrowPda = getEscrowPDA(args.projectId, program.programId);
771
+
772
+ return getMethods(program)
773
+ .cancelInvestment()
774
+ .accounts({
775
+ investor,
776
+ project: projectPda,
777
+ investment: investmentPda,
778
+ nftMint: nftMintPubkey,
779
+ investorNftAccount: args.investorNftAccount,
780
+ projectEscrow: args.escrowTokenAccount,
781
+ investorUsdcAccount: args.investorUsdcAccount,
782
+ escrowPda,
783
+ })
784
+ .rpc();
785
+ }
786
+
787
+ // =============================================================================
788
+ // Pivot Instructions
789
+ // =============================================================================
790
+
791
+ /**
792
+ * Propose a pivot
793
+ */
794
+ export async function proposePivot(
795
+ program: AnyProgram,
796
+ args: {
797
+ projectId: BN;
798
+ newMetadataUri: string;
799
+ newMilestones: Array<{ percentage: number; description: string }>;
800
+ },
801
+ founder: PublicKey
802
+ ): Promise<string> {
803
+ const projectPda = getProjectPDA(args.projectId, program.programId);
804
+
805
+ // Fetch project to get current pivot_count
806
+ const projectAccount = await getAccountNamespace(program).project.fetch(projectPda);
807
+ const pivotCount = projectAccount.pivotCount || 0;
808
+
809
+ // Derive pivot proposal PDA using pivot_count
810
+ const pivotProposalPda = getPivotProposalPDA(projectPda, pivotCount, program.programId);
811
+
812
+ return getMethods(program)
813
+ .proposePivot({
814
+ newMetadataUri: args.newMetadataUri,
815
+ newMilestones: args.newMilestones,
816
+ })
817
+ .accounts({
818
+ project: projectPda,
819
+ founder,
820
+ pivotProposal: pivotProposalPda,
821
+ systemProgram: SystemProgram.programId,
822
+ clock: SYSVAR_CLOCK_PUBKEY,
823
+ })
824
+ .rpc();
825
+ }
826
+
827
+ /**
828
+ * Approve pivot proposal (admin only)
829
+ */
830
+ export async function approvePivot(
831
+ program: AnyProgram,
832
+ projectId: BN,
833
+ adminKeypair: Keypair
834
+ ): Promise<string> {
835
+ const projectPda = getProjectPDA(projectId, program.programId);
836
+
837
+ // Fetch project to get the active pivot proposal
838
+ // The active_pivot field contains the actual pivot proposal pubkey
839
+ const projectAccount = await getAccountNamespace(program).project.fetch(projectPda);
840
+
841
+ // Use the active_pivot directly if available, otherwise derive from pivot_count
842
+ let pivotProposalPda: PublicKey;
843
+ if (projectAccount.activePivot) {
844
+ pivotProposalPda = projectAccount.activePivot;
845
+ } else {
846
+ // Fallback to deriving from pivot_count (pivot_count is NOT incremented until finalize)
847
+ const pivotCount = projectAccount.pivotCount || 0;
848
+ pivotProposalPda = getPivotProposalPDA(projectPda, pivotCount, program.programId);
849
+ }
850
+
851
+ return getMethods(program)
852
+ .approvePivot()
853
+ .accounts({
854
+ moderator: adminKeypair.publicKey,
855
+ project: projectPda,
856
+ pivotProposal: pivotProposalPda,
857
+ })
858
+ .signers([adminKeypair])
859
+ .rpc();
860
+ }
861
+
862
+ /**
863
+ * Withdraw from pivot during 7-day window
864
+ */
865
+ export async function withdrawFromPivot(
866
+ program: AnyProgram,
867
+ args: {
868
+ projectId: BN;
869
+ pivotCount: number; // Current pivot_count from project
870
+ nftMint: PublicKey;
871
+ investorTokenAccount: PublicKey;
872
+ escrowTokenAccount: PublicKey;
873
+ milestoneAccounts: PublicKey[]; // All milestone PDAs for calculating unreleased funds
874
+ },
875
+ investor: PublicKey
876
+ ): Promise<string> {
877
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
878
+ const projectPda = getProjectPDA(args.projectId, program.programId);
879
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
880
+ const escrowPda = getEscrowPDA(args.projectId, program.programId);
881
+ // Active pivot is at pivotCount (incremented only AFTER finalization)
882
+ const pivotProposalPda = getPivotProposalPDA(projectPda, args.pivotCount, program.programId);
883
+ // Get investor's NFT token account (ATA)
884
+ const investorNftAccount = getAssociatedTokenAddressSync(
885
+ nftMintPubkey,
886
+ investor,
887
+ false,
888
+ TOKEN_PROGRAM_ID
889
+ );
890
+
891
+ // Pass milestone accounts as remaining accounts for unreleased funds calculation
892
+ const remainingAccounts = args.milestoneAccounts.map((pubkey) => ({
893
+ pubkey: ensurePublicKey(pubkey),
894
+ isSigner: false,
895
+ isWritable: false,
896
+ }));
897
+
898
+ return getMethods(program)
899
+ .withdrawFromPivot()
900
+ .accounts({
901
+ investor,
902
+ project: projectPda,
903
+ pivotProposal: pivotProposalPda,
904
+ investment: investmentPda,
905
+ nftMint: nftMintPubkey,
906
+ investorNftAccount,
907
+ escrowTokenAccount: args.escrowTokenAccount,
908
+ investorTokenAccount: args.investorTokenAccount,
909
+ escrow: escrowPda,
910
+ })
911
+ .remainingAccounts(remainingAccounts)
912
+ .rpc();
913
+ }
914
+
915
+ /**
916
+ * Finalize pivot after 7-day window
917
+ *
918
+ * IMPORTANT: When old_milestone_count == new_milestone_count, the milestone PDAs are
919
+ * the same and get reinitialized in-place. In this case, only pass the milestone
920
+ * accounts once (not twice as old+new).
921
+ */
922
+ export async function finalizePivot(
923
+ program: AnyProgram,
924
+ args: {
925
+ projectId: BN;
926
+ pivotCount: number; // Current pivot_count from project (active pivot is at pivotCount)
927
+ milestoneAccounts: PublicKey[]; // All milestone PDAs (reused when old_count == new_count)
928
+ },
929
+ authority: PublicKey
930
+ ): Promise<string> {
931
+ const projectPda = getProjectPDA(args.projectId, program.programId);
932
+ // Active pivot is at pivotCount (incremented only AFTER finalization)
933
+ const pivotProposalPda = getPivotProposalPDA(projectPda, args.pivotCount, program.programId);
934
+
935
+ // Pass milestone accounts as remaining accounts
936
+ // When old_count == new_count, these are reinitialized in-place
937
+ const remainingAccounts = args.milestoneAccounts.map((pubkey) => ({
938
+ pubkey,
939
+ isSigner: false,
940
+ isWritable: true,
941
+ }));
942
+
943
+ return getMethods(program)
944
+ .finalizePivot()
945
+ .accounts({
946
+ authority,
947
+ project: projectPda,
948
+ pivotProposal: pivotProposalPda,
949
+ })
950
+ .remainingAccounts(remainingAccounts)
951
+ .rpc();
952
+ }
953
+
954
+ // =============================================================================
955
+ // TGE Instructions
956
+ // =============================================================================
957
+
958
+ /**
959
+ * Set TGE date and token mint
960
+ */
961
+ export async function setTgeDate(
962
+ program: AnyProgram,
963
+ args: {
964
+ projectId: BN;
965
+ tgeDate: BN;
966
+ tokenMint: PublicKey;
967
+ },
968
+ founder: PublicKey
969
+ ): Promise<string> {
970
+ const projectPda = getProjectPDA(args.projectId, program.programId);
971
+
972
+ return getMethods(program)
973
+ .setTgeDate({
974
+ tgeDate: args.tgeDate,
975
+ tokenMint: args.tokenMint,
976
+ })
977
+ .accounts({
978
+ project: projectPda,
979
+ founder,
980
+ })
981
+ .rpc();
982
+ }
983
+
984
+ /**
985
+ * Deposit tokens for investor distribution
986
+ */
987
+ export async function depositTokens(
988
+ program: AnyProgram,
989
+ args: {
990
+ projectId: BN;
991
+ amount: BN;
992
+ tokenMint: PublicKey;
993
+ founderTokenAccount: PublicKey;
994
+ },
995
+ founder: PublicKey
996
+ ): Promise<string> {
997
+ const projectPda = getProjectPDA(args.projectId, program.programId);
998
+
999
+ return getMethods(program)
1000
+ .depositTokens({ amount: args.amount })
1001
+ .accounts({
1002
+ project: projectPda,
1003
+ tokenMint: args.tokenMint,
1004
+ founderTokenAccount: args.founderTokenAccount,
1005
+ founder,
1006
+ })
1007
+ .rpc();
1008
+ }
1009
+
1010
+ /**
1011
+ * Claim project tokens using Investment NFT
1012
+ */
1013
+ export async function claimTokens(
1014
+ program: AnyProgram,
1015
+ args: {
1016
+ projectId: BN;
1017
+ nftMint: PublicKey;
1018
+ investorNftAccount: PublicKey;
1019
+ investorTokenAccount: PublicKey;
1020
+ projectTokenVault: PublicKey;
1021
+ },
1022
+ investor: PublicKey
1023
+ ): Promise<string> {
1024
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1025
+ const investmentPda = getInvestmentPDA(projectPda, args.nftMint, program.programId);
1026
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1027
+
1028
+ return getMethods(program)
1029
+ .claimTokens()
1030
+ .accounts({
1031
+ investor,
1032
+ project: projectPda,
1033
+ investment: investmentPda,
1034
+ investorNftAccount: args.investorNftAccount,
1035
+ projectTokenVault: args.projectTokenVault,
1036
+ investorTokenAccount: args.investorTokenAccount,
1037
+ tokenVaultPda,
1038
+ tokenProgram: TOKEN_PROGRAM_ID,
1039
+ })
1040
+ .rpc();
1041
+ }
1042
+
1043
+ /**
1044
+ * Report scam during 30-day post-TGE window
1045
+ */
1046
+ export async function reportScam(
1047
+ program: AnyProgram,
1048
+ args: {
1049
+ projectId: BN;
1050
+ nftMint: PublicKey;
1051
+ },
1052
+ reporter: PublicKey
1053
+ ): Promise<string> {
1054
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1055
+ const tgeEscrowPda = getTgeEscrowPDA(projectPda, program.programId);
1056
+ const investmentPda = getInvestmentPDA(projectPda, args.nftMint, program.programId);
1057
+
1058
+ return getMethods(program)
1059
+ .reportScam()
1060
+ .accounts({
1061
+ tgeEscrow: tgeEscrowPda,
1062
+ project: projectPda,
1063
+ investment: investmentPda,
1064
+ nftMint: args.nftMint,
1065
+ reporter,
1066
+ })
1067
+ .rpc();
1068
+ }
1069
+
1070
+ /**
1071
+ * Release 10% holdback to founder after 30 days
1072
+ */
1073
+ export async function releaseHoldback(
1074
+ program: AnyProgram,
1075
+ args: {
1076
+ projectId: BN;
1077
+ founderTokenAccount: PublicKey;
1078
+ }
1079
+ ): Promise<string> {
1080
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1081
+ const tgeEscrowPda = getTgeEscrowPDA(projectPda, program.programId);
1082
+
1083
+ return getMethods(program)
1084
+ .releaseHoldback()
1085
+ .accounts({
1086
+ tgeEscrow: tgeEscrowPda,
1087
+ project: projectPda,
1088
+ founderTokenAccount: args.founderTokenAccount,
1089
+ })
1090
+ .rpc();
1091
+ }
1092
+
1093
+ // =============================================================================
1094
+ // Abandonment Instructions
1095
+ // =============================================================================
1096
+
1097
+ /**
1098
+ * Check for abandonment (90 days inactivity)
1099
+ */
1100
+ export async function checkAbandonment(
1101
+ program: AnyProgram,
1102
+ projectId: BN,
1103
+ milestoneIndex: number = 0
1104
+ ): Promise<string> {
1105
+ const projectPda = getProjectPDA(projectId, program.programId);
1106
+ const milestonePda = getMilestonePDA(projectPda, milestoneIndex, program.programId);
1107
+
1108
+ return getMethods(program)
1109
+ .checkAbandonment()
1110
+ .accounts({
1111
+ project: projectPda,
1112
+ milestone: milestonePda,
1113
+ })
1114
+ .rpc();
1115
+ }
1116
+
1117
+ /**
1118
+ * Claim refund after abandonment
1119
+ *
1120
+ * @param milestoneCount - Number of milestones in the project (used to derive milestone PDAs for remainingAccounts)
1121
+ * The program calculates unreleased funds by iterating through milestone accounts.
1122
+ */
1123
+ export async function claimRefund(
1124
+ program: AnyProgram,
1125
+ args: {
1126
+ projectId: BN;
1127
+ nftMint: PublicKey;
1128
+ investorNftAccount: PublicKey;
1129
+ investorUsdcAccount: PublicKey;
1130
+ escrowTokenAccount: PublicKey;
1131
+ milestoneCount?: number; // If not provided, defaults to 1
1132
+ },
1133
+ investor: PublicKey
1134
+ ): Promise<string> {
1135
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
1136
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1137
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
1138
+
1139
+ // Derive milestone PDAs and pass as remainingAccounts
1140
+ // The program iterates through these to calculate unreleased funds
1141
+ const milestoneCount = args.milestoneCount ?? 1;
1142
+ const remainingAccounts = [];
1143
+ for (let i = 0; i < milestoneCount; i++) {
1144
+ const milestonePda = getMilestonePDA(projectPda, i, program.programId);
1145
+ remainingAccounts.push({
1146
+ pubkey: milestonePda,
1147
+ isWritable: false,
1148
+ isSigner: false,
1149
+ });
1150
+ }
1151
+
1152
+ return getMethods(program)
1153
+ .claimRefund()
1154
+ .accounts({
1155
+ project: projectPda,
1156
+ investment: investmentPda,
1157
+ nftMint: nftMintPubkey,
1158
+ investorNftAccount: args.investorNftAccount,
1159
+ investor,
1160
+ investorTokenAccount: args.investorUsdcAccount,
1161
+ escrowTokenAccount: args.escrowTokenAccount,
1162
+ })
1163
+ .remainingAccounts(remainingAccounts)
1164
+ .rpc();
1165
+ }
1166
+
1167
+ // =============================================================================
1168
+ // ZTM v2.0 Token Distribution Instructions
1169
+ // =============================================================================
1170
+
1171
+ /**
1172
+ * Claim investor tokens from a passed milestone (whitepaper: manual claim model)
1173
+ *
1174
+ * ZTM v2.0: Per whitepaper, investors manually claim their tokens after a milestone passes.
1175
+ * This replaces the batch distribution model with investor-initiated per-NFT claims.
1176
+ *
1177
+ * @param milestoneIndex - The milestone index to claim tokens from
1178
+ * @param nftMint - The NFT mint that proves investment ownership
1179
+ * @param investorTokenAccount - Investor's token account to receive claimed tokens
1180
+ */
1181
+ export async function claimInvestorTokens(
1182
+ program: AnyProgram,
1183
+ args: {
1184
+ projectId: BN;
1185
+ /** Milestone index to claim tokens from */
1186
+ milestoneIndex: number;
1187
+ /** NFT mint that proves investment ownership */
1188
+ nftMint: PublicKey;
1189
+ /** Investor's token account to receive claimed tokens */
1190
+ investorTokenAccount: PublicKey;
1191
+ },
1192
+ investor: PublicKey
1193
+ ): Promise<string> {
1194
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
1195
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1196
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1197
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
1198
+ const investorVaultPda = getInvestorVaultPDA(projectPda, program.programId);
1199
+ const vaultAuthorityPda = getVaultAuthorityPDA(projectPda, program.programId);
1200
+
1201
+ // Get investor's NFT token account (ATA)
1202
+ const investorNftAccount = getAssociatedTokenAddressSync(
1203
+ nftMintPubkey,
1204
+ investor,
1205
+ false,
1206
+ TOKEN_PROGRAM_ID
1207
+ );
1208
+
1209
+ return getMethods(program)
1210
+ .claimInvestorTokens({ milestoneIndex: args.milestoneIndex })
1211
+ .accounts({
1212
+ investor,
1213
+ project: projectPda,
1214
+ tokenVault: tokenVaultPda,
1215
+ investment: investmentPda,
1216
+ nftMint: nftMintPubkey,
1217
+ investorNftAccount,
1218
+ investorVault: investorVaultPda,
1219
+ investorTokenAccount: args.investorTokenAccount,
1220
+ vaultAuthority: vaultAuthorityPda,
1221
+ tokenProgram: TOKEN_PROGRAM_ID,
1222
+ })
1223
+ .rpc();
1224
+ }
1225
+
1226
+ /**
1227
+ * Distribute tokens to NFT holders for a milestone
1228
+ *
1229
+ * ZTM v2.0: Called by cranker after finalize_voting sets distribution_pending = true.
1230
+ * Processes batch of investments, transferring unlocked tokens to NFT holders.
1231
+ *
1232
+ * @deprecated Use claimInvestorTokens instead (whitepaper manual claim model)
1233
+ *
1234
+ * @param investments - Array of { investmentPda, investorTokenAccount } pairs
1235
+ * Each pair represents an investor's investment and their token account to receive tokens.
1236
+ * Max batch size: 10 investments per call.
1237
+ */
1238
+ export async function distributeTokens(
1239
+ program: AnyProgram,
1240
+ args: {
1241
+ projectId: BN;
1242
+ milestoneIndex: number;
1243
+ /** Investment and token account pairs to process */
1244
+ investments: Array<{
1245
+ investmentPda: PublicKey;
1246
+ investorTokenAccount: PublicKey;
1247
+ }>;
1248
+ },
1249
+ payer: PublicKey
1250
+ ): Promise<string> {
1251
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1252
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1253
+ const investorVaultPda = getInvestorVaultPDA(projectPda, program.programId);
1254
+ const vaultAuthorityPda = getVaultAuthorityPDA(projectPda, program.programId);
1255
+
1256
+ // Build remaining accounts: (Investment, TokenAccount) pairs
1257
+ const remainingAccounts = args.investments.flatMap((inv) => [
1258
+ { pubkey: inv.investmentPda, isSigner: false, isWritable: true },
1259
+ { pubkey: inv.investorTokenAccount, isSigner: false, isWritable: true },
1260
+ ]);
1261
+
1262
+ return getMethods(program)
1263
+ .distributeTokens({ milestoneIndex: args.milestoneIndex })
1264
+ .accounts({
1265
+ project: projectPda,
1266
+ tokenVault: tokenVaultPda,
1267
+ investorVault: investorVaultPda,
1268
+ vaultAuthority: vaultAuthorityPda,
1269
+ payer,
1270
+ tokenProgram: TOKEN_PROGRAM_ID,
1271
+ })
1272
+ .remainingAccounts(remainingAccounts)
1273
+ .rpc();
1274
+ }
1275
+
1276
+ /**
1277
+ * Complete token distribution for a milestone
1278
+ *
1279
+ * ZTM v2.0: Marks distribution as complete after all batches have been processed.
1280
+ * Permissionless - anyone can call this to finalize a distribution.
1281
+ */
1282
+ export async function completeDistribution(
1283
+ program: AnyProgram,
1284
+ args: {
1285
+ projectId: BN;
1286
+ milestoneIndex: number;
1287
+ },
1288
+ payer: PublicKey
1289
+ ): Promise<string> {
1290
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1291
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1292
+
1293
+ return getMethods(program)
1294
+ .completeDistribution({ milestoneIndex: args.milestoneIndex })
1295
+ .accounts({
1296
+ project: projectPda,
1297
+ tokenVault: tokenVaultPda,
1298
+ payer,
1299
+ })
1300
+ .rpc();
1301
+ }
1302
+
1303
+ // =============================================================================
1304
+ // Exit Window Instructions
1305
+ // =============================================================================
1306
+
1307
+ /**
1308
+ * Claim exit window refund during 3-failure voluntary exit window
1309
+ * Per whitepaper: 3 consecutive failures trigger 7-day voluntary exit window
1310
+ * Investors can claim proportional share of unreleased USDC escrow funds
1311
+ */
1312
+ export async function claimExitWindowRefund(
1313
+ program: AnyProgram,
1314
+ args: {
1315
+ projectId: BN;
1316
+ nftMint: PublicKey;
1317
+ investorNftAccount: PublicKey;
1318
+ escrowTokenAccount: PublicKey;
1319
+ investorTokenAccount: PublicKey;
1320
+ milestoneAccounts?: PublicKey[];
1321
+ },
1322
+ investor: PublicKey
1323
+ ): Promise<string> {
1324
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1325
+ const investmentPda = getInvestmentPDA(projectPda, args.nftMint, program.programId);
1326
+ const escrowPda = getEscrowPDA(args.projectId, program.programId);
1327
+
1328
+ const remainingAccounts = (args.milestoneAccounts || []).map((pubkey) => ({
1329
+ pubkey,
1330
+ isSigner: false,
1331
+ isWritable: false,
1332
+ }));
1333
+
1334
+ return getMethods(program)
1335
+ .claimExitWindowRefund()
1336
+ .accountsPartial({
1337
+ project: projectPda,
1338
+ investment: investmentPda,
1339
+ nftMint: args.nftMint,
1340
+ investorNftAccount: args.investorNftAccount,
1341
+ escrowTokenAccount: args.escrowTokenAccount,
1342
+ investorTokenAccount: args.investorTokenAccount,
1343
+ escrowPda,
1344
+ investor,
1345
+ })
1346
+ .remainingAccounts(remainingAccounts)
1347
+ .rpc();
1348
+ }
1349
+
1350
+ // =============================================================================
1351
+ // ZTM v2.0 Founder Vesting Instructions
1352
+ // =============================================================================
1353
+
1354
+ /**
1355
+ * Initialize founder vesting after MAE (Market Access Event)
1356
+ *
1357
+ * ZTM v2.0: Creates FounderVesting PDA with vesting schedule from Tokenomics.
1358
+ * Must be called after project reaches Completed state (all milestones done).
1359
+ * Permissionless - anyone can pay to initialize.
1360
+ */
1361
+ export async function initializeFounderVesting(
1362
+ program: AnyProgram,
1363
+ args: {
1364
+ projectId: BN;
1365
+ },
1366
+ payer: PublicKey
1367
+ ): Promise<string> {
1368
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1369
+ const tokenomicsPda = getTokenomicsPDA(projectPda, program.programId);
1370
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1371
+ const founderVestingPda = getFounderVestingPDA(projectPda, program.programId);
1372
+
1373
+ return getMethods(program)
1374
+ .initializeFounderVesting()
1375
+ .accounts({
1376
+ project: projectPda,
1377
+ tokenomics: tokenomicsPda,
1378
+ tokenVault: tokenVaultPda,
1379
+ founderVesting: founderVestingPda,
1380
+ payer,
1381
+ systemProgram: SystemProgram.programId,
1382
+ })
1383
+ .rpc();
1384
+ }
1385
+
1386
+ /**
1387
+ * Claim vested tokens from founder vault
1388
+ *
1389
+ * ZTM v2.0: Founder claims tokens based on linear vesting schedule.
1390
+ * Requires cliff period to pass before any tokens can be claimed.
1391
+ */
1392
+ export async function claimVestedTokens(
1393
+ program: AnyProgram,
1394
+ args: {
1395
+ projectId: BN;
1396
+ /** Founder's token account to receive vested tokens */
1397
+ founderTokenAccount: PublicKey;
1398
+ },
1399
+ founder: PublicKey
1400
+ ): Promise<string> {
1401
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1402
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1403
+ const founderVestingPda = getFounderVestingPDA(projectPda, program.programId);
1404
+ const founderVaultPda = getFounderVaultPDA(projectPda, program.programId);
1405
+ const vaultAuthorityPda = getVaultAuthorityPDA(projectPda, program.programId);
1406
+
1407
+ return getMethods(program)
1408
+ .claimVestedTokens()
1409
+ .accounts({
1410
+ project: projectPda,
1411
+ tokenVault: tokenVaultPda,
1412
+ founderVesting: founderVestingPda,
1413
+ founderVault: founderVaultPda,
1414
+ vaultAuthority: vaultAuthorityPda,
1415
+ founderTokenAccount: args.founderTokenAccount,
1416
+ founder,
1417
+ tokenProgram: TOKEN_PROGRAM_ID,
1418
+ })
1419
+ .rpc();
1420
+ }
1421
+
1422
+ // =============================================================================
1423
+ // ZTM v2.0 Circuit Breaker Instructions
1424
+ // =============================================================================
1425
+
1426
+ /**
1427
+ * Force complete a stuck distribution (admin only)
1428
+ *
1429
+ * ZTM v2.0: Circuit breaker for when token distribution is stuck for >7 days.
1430
+ * Marks distribution as complete so project can continue.
1431
+ * Affected investors can use claimMissedUnlock to get their tokens.
1432
+ */
1433
+ export async function forceCompleteDistribution(
1434
+ program: AnyProgram,
1435
+ args: {
1436
+ projectId: BN;
1437
+ },
1438
+ adminKeypair: Keypair
1439
+ ): Promise<string> {
1440
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1441
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1442
+ const adminConfigPda = getAdminConfigPDA(program.programId);
1443
+
1444
+ return getMethods(program)
1445
+ .forceCompleteDistribution()
1446
+ .accounts({
1447
+ admin: adminKeypair.publicKey,
1448
+ adminConfig: adminConfigPda,
1449
+ project: projectPda,
1450
+ tokenVault: tokenVaultPda,
1451
+ })
1452
+ .signers([adminKeypair])
1453
+ .rpc();
1454
+ }
1455
+
1456
+ /**
1457
+ * Claim missed token unlock after force-complete distribution
1458
+ *
1459
+ * ZTM v2.0: Allows investors to claim tokens they missed during a stuck
1460
+ * distribution that was force-completed by admin.
1461
+ */
1462
+ export async function claimMissedUnlock(
1463
+ program: AnyProgram,
1464
+ args: {
1465
+ projectId: BN;
1466
+ nftMint: PublicKey;
1467
+ /** Milestone index to claim for */
1468
+ milestoneIndex: number;
1469
+ /** Claimer's token account to receive tokens */
1470
+ claimerTokenAccount: PublicKey;
1471
+ },
1472
+ claimer: PublicKey
1473
+ ): Promise<string> {
1474
+ const nftMintPubkey = ensurePublicKey(args.nftMint);
1475
+ const projectPda = getProjectPDA(args.projectId, program.programId);
1476
+ const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
1477
+ const investmentPda = getInvestmentPDA(projectPda, nftMintPubkey, program.programId);
1478
+ const investorVaultPda = getInvestorVaultPDA(projectPda, program.programId);
1479
+ const vaultAuthorityPda = getVaultAuthorityPDA(projectPda, program.programId);
1480
+
1481
+ // Get claimer's NFT token account (ATA)
1482
+ const claimerNftAccount = getAssociatedTokenAddressSync(
1483
+ nftMintPubkey,
1484
+ claimer,
1485
+ false,
1486
+ TOKEN_PROGRAM_ID
1487
+ );
1488
+
1489
+ return getMethods(program)
1490
+ .claimMissedUnlock({ milestoneIndex: args.milestoneIndex })
1491
+ .accounts({
1492
+ claimer,
1493
+ project: projectPda,
1494
+ tokenVault: tokenVaultPda,
1495
+ investment: investmentPda,
1496
+ nftMint: nftMintPubkey,
1497
+ claimerNftAccount,
1498
+ investorVault: investorVaultPda,
1499
+ claimerTokenAccount: args.claimerTokenAccount,
1500
+ vaultAuthority: vaultAuthorityPda,
1501
+ tokenProgram: TOKEN_PROGRAM_ID,
1502
+ })
1503
+ .rpc();
1504
+ }