@zemyth/raise-sdk 0.1.1 → 0.1.3
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 +11 -9
- package/dist/accounts/index.cjs +531 -3
- package/dist/accounts/index.cjs.map +1 -1
- package/dist/accounts/index.d.cts +307 -2
- package/dist/accounts/index.d.ts +307 -2
- package/dist/accounts/index.js +503 -4
- package/dist/accounts/index.js.map +1 -1
- package/dist/constants/index.cjs +41 -3
- package/dist/constants/index.cjs.map +1 -1
- package/dist/constants/index.d.cts +38 -3
- package/dist/constants/index.d.ts +38 -3
- package/dist/constants/index.js +40 -4
- package/dist/constants/index.js.map +1 -1
- package/dist/index.cjs +2297 -361
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +566 -7
- package/dist/index.d.ts +566 -7
- package/dist/index.js +2279 -379
- package/dist/index.js.map +1 -1
- package/dist/instructions/index.cjs +783 -40
- package/dist/instructions/index.cjs.map +1 -1
- package/dist/instructions/index.d.cts +492 -6
- package/dist/instructions/index.d.ts +492 -6
- package/dist/instructions/index.js +762 -42
- package/dist/instructions/index.js.map +1 -1
- package/dist/pdas/index.cjs +163 -1
- package/dist/pdas/index.cjs.map +1 -1
- package/dist/pdas/index.d.cts +131 -1
- package/dist/pdas/index.d.ts +131 -1
- package/dist/pdas/index.js +151 -2
- package/dist/pdas/index.js.map +1 -1
- package/dist/types/index.cjs +9 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +586 -3
- package/dist/types/index.d.ts +586 -3
- package/dist/types/index.js +9 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/dynamic-tokenomics.test.ts +358 -0
- package/src/accounts/index.ts +852 -1
- package/src/client.ts +1130 -1
- package/src/constants/index.ts +48 -2
- package/src/index.ts +58 -0
- package/src/instructions/index.ts +1383 -40
- package/src/pdas/index.ts +346 -0
- package/src/types/index.ts +698 -2
- package/src/utils/index.ts +90 -0
package/src/accounts/index.ts
CHANGED
|
@@ -14,8 +14,20 @@ import {
|
|
|
14
14
|
getPivotProposalPDA,
|
|
15
15
|
getTgeEscrowPDA,
|
|
16
16
|
getAdminConfigPDA,
|
|
17
|
+
getTokenVaultPDA,
|
|
18
|
+
getTokenomicsPDA,
|
|
19
|
+
getAllocationProposalPDA,
|
|
20
|
+
getAllocationVotePDA,
|
|
21
|
+
getSubAllocationVestingPDA,
|
|
22
|
+
getInvestorMilestoneVestingPDA,
|
|
23
|
+
getFounderMilestoneVestingPDA,
|
|
24
|
+
getFundingRoundPDA,
|
|
25
|
+
getRoundMilestonePDA,
|
|
26
|
+
getRoundInvestmentPDA,
|
|
27
|
+
getRoundInvestorMilestoneVestingPDA,
|
|
28
|
+
getFutureRoundVaultPDA,
|
|
17
29
|
} from '../pdas/index.js';
|
|
18
|
-
import type { InvestmentWithKey, MilestoneWithKey, VoteWithKey } from '../types/index.js';
|
|
30
|
+
import type { InvestmentWithKey, MilestoneWithKey, VoteWithKey, AllocationProposalWithKey, FundingRoundWithKey } from '../types/index.js';
|
|
19
31
|
|
|
20
32
|
// Generic type for any Anchor program
|
|
21
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -306,6 +318,29 @@ export async function fetchAdminConfig(program: AnyProgram) {
|
|
|
306
318
|
return await getAccountNamespace(program).adminConfig.fetch(adminConfigPda);
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Fetch token vault account data
|
|
323
|
+
*
|
|
324
|
+
* @param program - Anchor program instance
|
|
325
|
+
* @param projectId - Project identifier
|
|
326
|
+
* @returns TokenVault account data or null if not found
|
|
327
|
+
*/
|
|
328
|
+
export async function fetchTokenVault(
|
|
329
|
+
program: AnyProgram,
|
|
330
|
+
projectId: BN
|
|
331
|
+
) {
|
|
332
|
+
try {
|
|
333
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
334
|
+
const tokenVaultPda = getTokenVaultPDA(projectPda, program.programId);
|
|
335
|
+
return await getAccountNamespace(program).tokenVault.fetch(tokenVaultPda);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
309
344
|
/**
|
|
310
345
|
* Check if an account exists
|
|
311
346
|
*
|
|
@@ -327,3 +362,819 @@ export async function accountExists(
|
|
|
327
362
|
return false;
|
|
328
363
|
}
|
|
329
364
|
}
|
|
365
|
+
|
|
366
|
+
// =============================================================================
|
|
367
|
+
// Dynamic Tokenomics Account Fetchers
|
|
368
|
+
// =============================================================================
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Fetch tokenomics account data
|
|
372
|
+
*
|
|
373
|
+
* @param program - Anchor program instance
|
|
374
|
+
* @param projectId - Project identifier
|
|
375
|
+
* @returns Tokenomics account data or null if not found
|
|
376
|
+
*/
|
|
377
|
+
export async function fetchTokenomics(
|
|
378
|
+
program: AnyProgram,
|
|
379
|
+
projectId: BN
|
|
380
|
+
) {
|
|
381
|
+
try {
|
|
382
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
383
|
+
const tokenomicsPda = getTokenomicsPDA(projectPda, program.programId);
|
|
384
|
+
return await getAccountNamespace(program).tokenomics.fetch(tokenomicsPda);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Fetch allocation proposal account data
|
|
395
|
+
*
|
|
396
|
+
* @param program - Anchor program instance
|
|
397
|
+
* @param projectId - Project identifier
|
|
398
|
+
* @param proposalIndex - Proposal index
|
|
399
|
+
* @returns AllocationProposal account data or null if not found
|
|
400
|
+
*/
|
|
401
|
+
export async function fetchAllocationProposal(
|
|
402
|
+
program: AnyProgram,
|
|
403
|
+
projectId: BN,
|
|
404
|
+
proposalIndex: number
|
|
405
|
+
) {
|
|
406
|
+
try {
|
|
407
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
408
|
+
const proposalPda = getAllocationProposalPDA(projectPda, proposalIndex, program.programId);
|
|
409
|
+
return await getAccountNamespace(program).allocationProposal.fetch(proposalPda);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Fetch all allocation proposals for a project
|
|
420
|
+
*
|
|
421
|
+
* @param program - Anchor program instance
|
|
422
|
+
* @param projectId - Project identifier
|
|
423
|
+
* @returns Array of allocation proposal accounts with their public keys
|
|
424
|
+
*/
|
|
425
|
+
export async function fetchAllAllocationProposals(
|
|
426
|
+
program: AnyProgram,
|
|
427
|
+
projectId: BN
|
|
428
|
+
): Promise<AllocationProposalWithKey[]> {
|
|
429
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
430
|
+
|
|
431
|
+
const proposals = await getAccountNamespace(program).allocationProposal.all([
|
|
432
|
+
{
|
|
433
|
+
memcmp: {
|
|
434
|
+
offset: 8, // Skip discriminator
|
|
435
|
+
bytes: projectPda.toBase58(),
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
return proposals.map((p: { publicKey: PublicKey; account: unknown }) => ({
|
|
441
|
+
publicKey: p.publicKey,
|
|
442
|
+
account: p.account,
|
|
443
|
+
})) as AllocationProposalWithKey[];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Fetch allocation vote account data
|
|
448
|
+
*
|
|
449
|
+
* @param program - Anchor program instance
|
|
450
|
+
* @param projectId - Project identifier
|
|
451
|
+
* @param proposalIndex - Proposal index
|
|
452
|
+
* @param nftMint - NFT mint used for voting
|
|
453
|
+
* @returns AllocationVote account data or null if not found
|
|
454
|
+
*/
|
|
455
|
+
export async function fetchAllocationVote(
|
|
456
|
+
program: AnyProgram,
|
|
457
|
+
projectId: BN,
|
|
458
|
+
proposalIndex: number,
|
|
459
|
+
nftMint: PublicKey
|
|
460
|
+
) {
|
|
461
|
+
try {
|
|
462
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
463
|
+
const proposalPda = getAllocationProposalPDA(projectPda, proposalIndex, program.programId);
|
|
464
|
+
const votePda = getAllocationVotePDA(proposalPda, nftMint, program.programId);
|
|
465
|
+
return await getAccountNamespace(program).allocationVote.fetch(votePda);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Fetch sub-allocation vesting account data
|
|
476
|
+
*
|
|
477
|
+
* @param program - Anchor program instance
|
|
478
|
+
* @param projectId - Project identifier
|
|
479
|
+
* @param subAllocationId - Sub-allocation ID (0-9)
|
|
480
|
+
* @returns SubAllocationVesting account data or null if not found
|
|
481
|
+
*/
|
|
482
|
+
export async function fetchSubAllocationVesting(
|
|
483
|
+
program: AnyProgram,
|
|
484
|
+
projectId: BN,
|
|
485
|
+
subAllocationId: number
|
|
486
|
+
) {
|
|
487
|
+
try {
|
|
488
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
489
|
+
const vestingPda = getSubAllocationVestingPDA(projectPda, subAllocationId, program.programId);
|
|
490
|
+
return await getAccountNamespace(program).subAllocationVesting.fetch(vestingPda);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// Per-Milestone Vesting Account Fetchers
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Fetch investor milestone vesting account data
|
|
505
|
+
*
|
|
506
|
+
* Per-milestone vesting: Each investor has a separate vesting PDA for each
|
|
507
|
+
* milestone they claim tokens from.
|
|
508
|
+
*
|
|
509
|
+
* @param program - Anchor program instance
|
|
510
|
+
* @param projectId - Project identifier
|
|
511
|
+
* @param milestoneIndex - Milestone index
|
|
512
|
+
* @param nftMint - NFT mint that proves investment ownership
|
|
513
|
+
* @returns InvestorMilestoneVesting account data or null if not found
|
|
514
|
+
*/
|
|
515
|
+
export async function fetchInvestorMilestoneVesting(
|
|
516
|
+
program: AnyProgram,
|
|
517
|
+
projectId: BN,
|
|
518
|
+
milestoneIndex: number,
|
|
519
|
+
nftMint: PublicKey
|
|
520
|
+
) {
|
|
521
|
+
try {
|
|
522
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
523
|
+
const investmentPda = getInvestmentPDA(projectPda, nftMint, program.programId);
|
|
524
|
+
const vestingPda = getInvestorMilestoneVestingPDA(projectPda, milestoneIndex, investmentPda, program.programId);
|
|
525
|
+
return await getAccountNamespace(program).investorMilestoneVesting.fetch(vestingPda);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Fetch founder milestone vesting account data
|
|
536
|
+
*
|
|
537
|
+
* Per-milestone vesting: Each milestone has a separate vesting PDA for the
|
|
538
|
+
* founder's milestone-based allocation.
|
|
539
|
+
*
|
|
540
|
+
* @param program - Anchor program instance
|
|
541
|
+
* @param projectId - Project identifier
|
|
542
|
+
* @param milestoneIndex - Milestone index
|
|
543
|
+
* @returns FounderMilestoneVesting account data or null if not found
|
|
544
|
+
*/
|
|
545
|
+
export async function fetchFounderMilestoneVesting(
|
|
546
|
+
program: AnyProgram,
|
|
547
|
+
projectId: BN,
|
|
548
|
+
milestoneIndex: number
|
|
549
|
+
) {
|
|
550
|
+
try {
|
|
551
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
552
|
+
const vestingPda = getFounderMilestoneVestingPDA(projectPda, milestoneIndex, program.programId);
|
|
553
|
+
return await getAccountNamespace(program).founderMilestoneVesting.fetch(vestingPda);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// =============================================================================
|
|
563
|
+
// Early Token Release Helpers
|
|
564
|
+
// =============================================================================
|
|
565
|
+
|
|
566
|
+
/** Early token cooling period in seconds (24 hours in production, 10s in dev) */
|
|
567
|
+
export const EARLY_TOKEN_COOLING_PERIOD_SECONDS = 86_400; // 24 hours
|
|
568
|
+
export const EARLY_TOKEN_COOLING_PERIOD_SECONDS_DEV = 10; // 10 seconds for dev
|
|
569
|
+
|
|
570
|
+
/** Early token release percentage in basis points (500 = 5%) */
|
|
571
|
+
export const EARLY_TOKEN_RELEASE_BPS = 500;
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Check if an investor can claim early tokens
|
|
575
|
+
*
|
|
576
|
+
* Early Token Release: Investors can claim 5% of their token allocation
|
|
577
|
+
* 24 hours after investing. This helper checks eligibility.
|
|
578
|
+
*
|
|
579
|
+
* @param investment - Investment account data
|
|
580
|
+
* @param currentTimestamp - Current Unix timestamp (optional, uses Date.now() if not provided)
|
|
581
|
+
* @param isDev - Use dev mode (10s) or production mode (24h) cooling period
|
|
582
|
+
* @returns Object with canClaim boolean, reason if ineligible, and time remaining if cooling period not expired
|
|
583
|
+
*/
|
|
584
|
+
export function canClaimEarlyTokens(
|
|
585
|
+
investment: {
|
|
586
|
+
earlyTokensClaimed: boolean;
|
|
587
|
+
investedAt: BN | number;
|
|
588
|
+
votingRightsActive?: boolean;
|
|
589
|
+
withdrawnFromPivot?: boolean;
|
|
590
|
+
},
|
|
591
|
+
currentTimestamp?: number,
|
|
592
|
+
isDev: boolean = false
|
|
593
|
+
): { canClaim: boolean; reason?: string; timeRemainingSeconds?: number } {
|
|
594
|
+
const now = currentTimestamp ?? Math.floor(Date.now() / 1000);
|
|
595
|
+
const coolingPeriod = isDev ? EARLY_TOKEN_COOLING_PERIOD_SECONDS_DEV : EARLY_TOKEN_COOLING_PERIOD_SECONDS;
|
|
596
|
+
|
|
597
|
+
// Check if already claimed
|
|
598
|
+
if (investment.earlyTokensClaimed) {
|
|
599
|
+
return { canClaim: false, reason: 'Early tokens already claimed' };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check if voting rights are active
|
|
603
|
+
if (investment.votingRightsActive === false) {
|
|
604
|
+
return { canClaim: false, reason: 'Voting rights have been revoked' };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Check if withdrawn from pivot
|
|
608
|
+
if (investment.withdrawnFromPivot === true) {
|
|
609
|
+
return { canClaim: false, reason: 'Investment has been withdrawn from pivot' };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Check cooling period
|
|
613
|
+
const investedAt = typeof investment.investedAt === 'number'
|
|
614
|
+
? investment.investedAt
|
|
615
|
+
: investment.investedAt.toNumber();
|
|
616
|
+
|
|
617
|
+
const coolingEndTime = investedAt + coolingPeriod;
|
|
618
|
+
|
|
619
|
+
if (now < coolingEndTime) {
|
|
620
|
+
const timeRemaining = coolingEndTime - now;
|
|
621
|
+
return {
|
|
622
|
+
canClaim: false,
|
|
623
|
+
reason: `Cooling period not expired. ${formatTimeRemaining(timeRemaining)} remaining.`,
|
|
624
|
+
timeRemainingSeconds: timeRemaining,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return { canClaim: true };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Check if a founder can claim early tokens
|
|
633
|
+
*
|
|
634
|
+
* Early Token Release: Founders can claim 5% of their token allocation
|
|
635
|
+
* when the project becomes Funded. This helper checks eligibility.
|
|
636
|
+
*
|
|
637
|
+
* @param project - Project account data
|
|
638
|
+
* @returns Object with canClaim boolean and reason if ineligible
|
|
639
|
+
*/
|
|
640
|
+
export function canClaimFounderEarlyTokens(
|
|
641
|
+
project: {
|
|
642
|
+
state: unknown;
|
|
643
|
+
founderEarlyTokensClaimed: boolean;
|
|
644
|
+
}
|
|
645
|
+
): { canClaim: boolean; reason?: string } {
|
|
646
|
+
// Check if already claimed
|
|
647
|
+
if (project.founderEarlyTokensClaimed) {
|
|
648
|
+
return { canClaim: false, reason: 'Founder early tokens already claimed' };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Check project state - must be Funded, InProgress, or Completed
|
|
652
|
+
// These states are represented as objects with a single key in Anchor
|
|
653
|
+
const stateStr = getProjectStateString(project.state);
|
|
654
|
+
const validStates = ['funded', 'inProgress', 'completed'];
|
|
655
|
+
|
|
656
|
+
if (!validStates.includes(stateStr)) {
|
|
657
|
+
return {
|
|
658
|
+
canClaim: false,
|
|
659
|
+
reason: `Project must be in Funded, InProgress, or Completed state. Current state: ${stateStr}`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return { canClaim: true };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Check if a founder can claim milestone-based tokens
|
|
668
|
+
*
|
|
669
|
+
* Early Token Release: Founders can claim milestone-based tokens when
|
|
670
|
+
* milestones are unlocked. This helper checks eligibility.
|
|
671
|
+
*
|
|
672
|
+
* @param milestone - Milestone account data
|
|
673
|
+
* @returns Object with canClaim boolean and reason if ineligible
|
|
674
|
+
*/
|
|
675
|
+
export function canClaimFounderMilestoneTokens(
|
|
676
|
+
milestone: {
|
|
677
|
+
state: unknown;
|
|
678
|
+
}
|
|
679
|
+
): { canClaim: boolean; reason?: string } {
|
|
680
|
+
// Check milestone state - must be Unlocked
|
|
681
|
+
const stateStr = getMilestoneStateString(milestone.state);
|
|
682
|
+
if (stateStr !== 'unlocked') {
|
|
683
|
+
return {
|
|
684
|
+
canClaim: false,
|
|
685
|
+
reason: `Milestone must be in Unlocked state. Current state: ${stateStr}`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return { canClaim: true };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Check if an investor needs to burn tokens for a refund
|
|
694
|
+
*
|
|
695
|
+
* Early Token Release: If the project has no milestones claimed (cumulative_percentage == 0)
|
|
696
|
+
* AND the investor has claimed early tokens, they must burn those tokens to get a full refund.
|
|
697
|
+
*
|
|
698
|
+
* @param project - Project account data
|
|
699
|
+
* @param investment - Investment account data
|
|
700
|
+
* @returns Object indicating whether burn is required
|
|
701
|
+
*/
|
|
702
|
+
export function requiresBurnForRefund(
|
|
703
|
+
project: {
|
|
704
|
+
cumulativePercentage: number;
|
|
705
|
+
},
|
|
706
|
+
investment: {
|
|
707
|
+
earlyTokensClaimed: boolean;
|
|
708
|
+
earlyTokensAmount: BN | number;
|
|
709
|
+
}
|
|
710
|
+
): { requiresBurn: boolean; burnAmount: number } {
|
|
711
|
+
const isFullRefund = project.cumulativePercentage === 0;
|
|
712
|
+
const requiresBurn = isFullRefund && investment.earlyTokensClaimed;
|
|
713
|
+
|
|
714
|
+
if (!requiresBurn) {
|
|
715
|
+
return { requiresBurn: false, burnAmount: 0 };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const burnAmount = typeof investment.earlyTokensAmount === 'number'
|
|
719
|
+
? investment.earlyTokensAmount
|
|
720
|
+
: investment.earlyTokensAmount.toNumber();
|
|
721
|
+
|
|
722
|
+
return { requiresBurn: true, burnAmount };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Calculate early token amount for an investment
|
|
727
|
+
*
|
|
728
|
+
* @param tokensAllocated - Total tokens allocated to the investment
|
|
729
|
+
* @returns Early token amount (5% of allocation)
|
|
730
|
+
*/
|
|
731
|
+
export function calculateEarlyTokenAmount(tokensAllocated: BN | number): BN {
|
|
732
|
+
const allocated = typeof tokensAllocated === 'number'
|
|
733
|
+
? new BN(tokensAllocated)
|
|
734
|
+
: tokensAllocated;
|
|
735
|
+
|
|
736
|
+
return allocated.muln(EARLY_TOKEN_RELEASE_BPS).divn(10000);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Calculate remaining token allocation after early tokens are claimed
|
|
741
|
+
*
|
|
742
|
+
* @param tokensAllocated - Total tokens allocated
|
|
743
|
+
* @returns Remaining allocation (95% of total)
|
|
744
|
+
*/
|
|
745
|
+
export function calculateRemainingAllocation(tokensAllocated: BN | number): BN {
|
|
746
|
+
const allocated = typeof tokensAllocated === 'number'
|
|
747
|
+
? new BN(tokensAllocated)
|
|
748
|
+
: tokensAllocated;
|
|
749
|
+
|
|
750
|
+
const remainingBps = 10000 - EARLY_TOKEN_RELEASE_BPS; // 9500 = 95%
|
|
751
|
+
return allocated.muln(remainingBps).divn(10000);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// =============================================================================
|
|
755
|
+
// Helper Functions
|
|
756
|
+
// =============================================================================
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Format time remaining in human-readable format
|
|
760
|
+
*/
|
|
761
|
+
function formatTimeRemaining(seconds: number): string {
|
|
762
|
+
if (seconds <= 0) return '0 seconds';
|
|
763
|
+
|
|
764
|
+
const hours = Math.floor(seconds / 3600);
|
|
765
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
766
|
+
const secs = seconds % 60;
|
|
767
|
+
|
|
768
|
+
const parts: string[] = [];
|
|
769
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
770
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
771
|
+
if (secs > 0 && hours === 0) parts.push(`${secs}s`);
|
|
772
|
+
|
|
773
|
+
return parts.join(' ') || '0 seconds';
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Convert Anchor project state object to string
|
|
778
|
+
*/
|
|
779
|
+
function getProjectStateString(state: unknown): string {
|
|
780
|
+
if (typeof state === 'object' && state !== null) {
|
|
781
|
+
const keys = Object.keys(state);
|
|
782
|
+
return keys[0]?.toLowerCase() ?? 'unknown';
|
|
783
|
+
}
|
|
784
|
+
return 'unknown';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Convert Anchor milestone state object to string
|
|
789
|
+
*/
|
|
790
|
+
function getMilestoneStateString(state: unknown): string {
|
|
791
|
+
if (typeof state === 'object' && state !== null) {
|
|
792
|
+
const keys = Object.keys(state);
|
|
793
|
+
return keys[0]?.toLowerCase() ?? 'unknown';
|
|
794
|
+
}
|
|
795
|
+
return 'unknown';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// =============================================================================
|
|
799
|
+
// Multi-Round Fundraising Account Fetchers
|
|
800
|
+
// =============================================================================
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Fetch FundingRound account data
|
|
804
|
+
*
|
|
805
|
+
* @param program - Anchor program instance
|
|
806
|
+
* @param projectId - Project identifier
|
|
807
|
+
* @param roundNumber - Round number (2, 3, 4...)
|
|
808
|
+
* @returns FundingRound account data or null if not found
|
|
809
|
+
*/
|
|
810
|
+
export async function fetchFundingRound(
|
|
811
|
+
program: AnyProgram,
|
|
812
|
+
projectId: BN,
|
|
813
|
+
roundNumber: number
|
|
814
|
+
) {
|
|
815
|
+
try {
|
|
816
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
817
|
+
const fundingRoundPda = getFundingRoundPDA(projectPda, roundNumber, program.programId);
|
|
818
|
+
return await getAccountNamespace(program).fundingRound.fetch(fundingRoundPda);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Fetch all FundingRounds for a project
|
|
829
|
+
*
|
|
830
|
+
* @param program - Anchor program instance
|
|
831
|
+
* @param projectId - Project identifier
|
|
832
|
+
* @returns Array of FundingRound accounts with their public keys
|
|
833
|
+
*/
|
|
834
|
+
export async function fetchAllFundingRounds(
|
|
835
|
+
program: AnyProgram,
|
|
836
|
+
projectId: BN
|
|
837
|
+
): Promise<FundingRoundWithKey[]> {
|
|
838
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
839
|
+
|
|
840
|
+
const rounds = await getAccountNamespace(program).fundingRound.all([
|
|
841
|
+
{
|
|
842
|
+
memcmp: {
|
|
843
|
+
offset: 8, // Skip discriminator
|
|
844
|
+
bytes: projectPda.toBase58(),
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
]);
|
|
848
|
+
|
|
849
|
+
return rounds.map((r: { publicKey: PublicKey; account: unknown }) => ({
|
|
850
|
+
publicKey: r.publicKey,
|
|
851
|
+
account: r.account,
|
|
852
|
+
})) as FundingRoundWithKey[];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Fetch round milestone account data
|
|
857
|
+
*
|
|
858
|
+
* @param program - Anchor program instance
|
|
859
|
+
* @param projectId - Project identifier
|
|
860
|
+
* @param roundNumber - Round number
|
|
861
|
+
* @param milestoneIndex - Milestone index
|
|
862
|
+
* @returns Milestone account data or null if not found
|
|
863
|
+
*/
|
|
864
|
+
export async function fetchRoundMilestone(
|
|
865
|
+
program: AnyProgram,
|
|
866
|
+
projectId: BN,
|
|
867
|
+
roundNumber: number,
|
|
868
|
+
milestoneIndex: number
|
|
869
|
+
) {
|
|
870
|
+
try {
|
|
871
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
872
|
+
const milestonePda = getRoundMilestonePDA(projectPda, roundNumber, milestoneIndex, program.programId);
|
|
873
|
+
return await getAccountNamespace(program).milestone.fetch(milestonePda);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Fetch round investment account data
|
|
884
|
+
*
|
|
885
|
+
* @param program - Anchor program instance
|
|
886
|
+
* @param projectId - Project identifier
|
|
887
|
+
* @param roundNumber - Round number
|
|
888
|
+
* @param nftMint - Investment NFT mint address
|
|
889
|
+
* @returns Investment account data or null if not found
|
|
890
|
+
*/
|
|
891
|
+
export async function fetchRoundInvestment(
|
|
892
|
+
program: AnyProgram,
|
|
893
|
+
projectId: BN,
|
|
894
|
+
roundNumber: number,
|
|
895
|
+
nftMint: PublicKey
|
|
896
|
+
) {
|
|
897
|
+
try {
|
|
898
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
899
|
+
const investmentPda = getRoundInvestmentPDA(projectPda, roundNumber, nftMint, program.programId);
|
|
900
|
+
return await getAccountNamespace(program).investment.fetch(investmentPda);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Fetch round investor milestone vesting account data
|
|
911
|
+
*
|
|
912
|
+
* @param program - Anchor program instance
|
|
913
|
+
* @param projectId - Project identifier
|
|
914
|
+
* @param roundNumber - Round number
|
|
915
|
+
* @param milestoneIndex - Milestone index
|
|
916
|
+
* @param nftMint - NFT mint that proves investment ownership
|
|
917
|
+
* @returns InvestorMilestoneVesting account data or null if not found
|
|
918
|
+
*/
|
|
919
|
+
export async function fetchRoundInvestorMilestoneVesting(
|
|
920
|
+
program: AnyProgram,
|
|
921
|
+
projectId: BN,
|
|
922
|
+
roundNumber: number,
|
|
923
|
+
milestoneIndex: number,
|
|
924
|
+
nftMint: PublicKey
|
|
925
|
+
) {
|
|
926
|
+
try {
|
|
927
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
928
|
+
const investmentPda = getRoundInvestmentPDA(projectPda, roundNumber, nftMint, program.programId);
|
|
929
|
+
const vestingPda = getRoundInvestorMilestoneVestingPDA(
|
|
930
|
+
projectPda,
|
|
931
|
+
roundNumber,
|
|
932
|
+
milestoneIndex,
|
|
933
|
+
investmentPda,
|
|
934
|
+
program.programId
|
|
935
|
+
);
|
|
936
|
+
return await getAccountNamespace(program).investorMilestoneVesting.fetch(vestingPda);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Fetch FutureRoundVault account data
|
|
947
|
+
*
|
|
948
|
+
* @param program - Anchor program instance
|
|
949
|
+
* @param projectId - Project identifier
|
|
950
|
+
* @returns FutureRoundVault account data or null if not found
|
|
951
|
+
*/
|
|
952
|
+
export async function fetchFutureRoundVault(
|
|
953
|
+
program: AnyProgram,
|
|
954
|
+
projectId: BN
|
|
955
|
+
) {
|
|
956
|
+
try {
|
|
957
|
+
const projectPda = getProjectPDA(projectId, program.programId);
|
|
958
|
+
const vaultPda = getFutureRoundVaultPDA(projectPda, program.programId);
|
|
959
|
+
return await getAccountNamespace(program).futureRoundVault.fetch(vaultPda);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error instanceof Error && error.message?.includes('Account does not exist')) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Check if an investor can claim R2+ early tokens
|
|
970
|
+
*
|
|
971
|
+
* @param investment - Investment account data (from R2+ round)
|
|
972
|
+
* @param currentTimestamp - Current Unix timestamp
|
|
973
|
+
* @param isDev - Use dev mode cooling period
|
|
974
|
+
* @returns Eligibility check result
|
|
975
|
+
*/
|
|
976
|
+
export function canClaimRoundEarlyTokens(
|
|
977
|
+
investment: {
|
|
978
|
+
earlyTokensClaimed: boolean;
|
|
979
|
+
investedAt: BN | number;
|
|
980
|
+
votingRightsActive?: boolean;
|
|
981
|
+
roundNumber: number;
|
|
982
|
+
},
|
|
983
|
+
currentTimestamp?: number,
|
|
984
|
+
isDev: boolean = false
|
|
985
|
+
): { canClaim: boolean; reason?: string; timeRemainingSeconds?: number } {
|
|
986
|
+
// Must be R2+ investment
|
|
987
|
+
if (investment.roundNumber < 2) {
|
|
988
|
+
return { canClaim: false, reason: 'R1 investments use claim_investor_early_tokens instruction' };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Delegate to the common early token check
|
|
992
|
+
return canClaimEarlyTokens(investment, currentTimestamp, isDev);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// =============================================================================
|
|
996
|
+
// Multi-Round Funding Eligibility Helpers
|
|
997
|
+
// =============================================================================
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Check if R1 (Project-based round) is fully funded
|
|
1001
|
+
*
|
|
1002
|
+
* @param project - Project account data
|
|
1003
|
+
* @returns True if total_raised >= funding_goal
|
|
1004
|
+
*/
|
|
1005
|
+
export function isR1FullyFunded(
|
|
1006
|
+
project: {
|
|
1007
|
+
totalRaised: BN | number;
|
|
1008
|
+
fundingGoal: BN | number;
|
|
1009
|
+
}
|
|
1010
|
+
): boolean {
|
|
1011
|
+
const raised = typeof project.totalRaised === 'number'
|
|
1012
|
+
? new BN(project.totalRaised)
|
|
1013
|
+
: project.totalRaised;
|
|
1014
|
+
const goal = typeof project.fundingGoal === 'number'
|
|
1015
|
+
? new BN(project.fundingGoal)
|
|
1016
|
+
: project.fundingGoal;
|
|
1017
|
+
|
|
1018
|
+
return raised.gte(goal);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Check if a FundingRound (R2+) is fully funded
|
|
1023
|
+
*
|
|
1024
|
+
* A round is considered "fully funded" when it reaches the Funded, InProgress, or Completed state.
|
|
1025
|
+
*
|
|
1026
|
+
* @param fundingRound - FundingRound account data
|
|
1027
|
+
* @returns True if round state is Funded, InProgress, or Completed
|
|
1028
|
+
*/
|
|
1029
|
+
export function isFundingRoundFullyFunded(
|
|
1030
|
+
fundingRound: {
|
|
1031
|
+
state: unknown;
|
|
1032
|
+
totalRaised?: BN | number;
|
|
1033
|
+
fundingGoal?: BN | number;
|
|
1034
|
+
}
|
|
1035
|
+
): boolean {
|
|
1036
|
+
const stateStr = getRoundStateString(fundingRound.state);
|
|
1037
|
+
return ['funded', 'inProgress', 'inprogress', 'completed'].includes(stateStr);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Check if a round (R1 or R2+) is fully funded
|
|
1042
|
+
*
|
|
1043
|
+
* For R1: Checks project.totalRaised >= project.fundingGoal
|
|
1044
|
+
* For R2+: Checks fundingRound.state is Funded or beyond
|
|
1045
|
+
*
|
|
1046
|
+
* @param program - Anchor program instance
|
|
1047
|
+
* @param projectId - Project identifier
|
|
1048
|
+
* @param roundNumber - Round number (1 for R1, 2+ for subsequent rounds)
|
|
1049
|
+
* @returns True if the round is fully funded
|
|
1050
|
+
*/
|
|
1051
|
+
export async function isRoundFullyFunded(
|
|
1052
|
+
program: AnyProgram,
|
|
1053
|
+
projectId: BN,
|
|
1054
|
+
roundNumber: number
|
|
1055
|
+
): Promise<boolean> {
|
|
1056
|
+
if (roundNumber === 1) {
|
|
1057
|
+
// R1 uses Project state
|
|
1058
|
+
const project = await fetchProject(program, projectId);
|
|
1059
|
+
if (!project) return false;
|
|
1060
|
+
return isR1FullyFunded(project);
|
|
1061
|
+
} else {
|
|
1062
|
+
// R2+ uses FundingRound state
|
|
1063
|
+
const fundingRound = await fetchFundingRound(program, projectId, roundNumber);
|
|
1064
|
+
if (!fundingRound) return false;
|
|
1065
|
+
return isFundingRoundFullyFunded(fundingRound);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Check if a project can open the next funding round
|
|
1071
|
+
*
|
|
1072
|
+
* Requirements:
|
|
1073
|
+
* 1. Project must be InProgress or Completed
|
|
1074
|
+
* 2. At least 1 milestone must have passed (for InProgress projects)
|
|
1075
|
+
* 3. Current round must be fully funded
|
|
1076
|
+
* 4. No other round is currently in Open state
|
|
1077
|
+
* 5. Future round allocation must be available
|
|
1078
|
+
*
|
|
1079
|
+
* @param program - Anchor program instance
|
|
1080
|
+
* @param projectId - Project identifier
|
|
1081
|
+
* @returns Eligibility check result with reason if ineligible
|
|
1082
|
+
*/
|
|
1083
|
+
export async function canOpenNextRound(
|
|
1084
|
+
program: AnyProgram,
|
|
1085
|
+
projectId: BN
|
|
1086
|
+
): Promise<{ canOpen: boolean; reason?: string; nextRoundNumber?: number }> {
|
|
1087
|
+
// Fetch project
|
|
1088
|
+
const project = await fetchProject(program, projectId);
|
|
1089
|
+
if (!project) {
|
|
1090
|
+
return { canOpen: false, reason: 'Project not found' };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const projectState = getProjectStateString(project.state);
|
|
1094
|
+
|
|
1095
|
+
// 1. Project must be InProgress or Completed
|
|
1096
|
+
if (!['inProgress', 'inprogress', 'completed'].includes(projectState)) {
|
|
1097
|
+
return {
|
|
1098
|
+
canOpen: false,
|
|
1099
|
+
reason: `Project must be in InProgress or Completed state. Current: ${projectState}`,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// 2. At least 1 milestone must have passed (for InProgress)
|
|
1104
|
+
if (projectState !== 'completed' && project.milestonesPassed === 0) {
|
|
1105
|
+
return {
|
|
1106
|
+
canOpen: false,
|
|
1107
|
+
reason: 'At least one milestone must pass before opening next round',
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 3. No other round currently active
|
|
1112
|
+
if (project.activeRound !== 0) {
|
|
1113
|
+
return {
|
|
1114
|
+
canOpen: false,
|
|
1115
|
+
reason: `Round ${project.activeRound} is already active`,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// 4. Current round must be fully funded
|
|
1120
|
+
const currentRound = project.roundCount;
|
|
1121
|
+
const isCurrentFunded = await isRoundFullyFunded(program, projectId, currentRound);
|
|
1122
|
+
if (!isCurrentFunded) {
|
|
1123
|
+
return {
|
|
1124
|
+
canOpen: false,
|
|
1125
|
+
reason: `Current round ${currentRound} must be fully funded before opening next round`,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// 5. Future round allocation must be available
|
|
1130
|
+
const tokenomics = await fetchTokenomics(program, projectId);
|
|
1131
|
+
if (!tokenomics) {
|
|
1132
|
+
return { canOpen: false, reason: 'Tokenomics not found' };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (tokenomics.futureRoundAllocationBps === 0) {
|
|
1136
|
+
return {
|
|
1137
|
+
canOpen: false,
|
|
1138
|
+
reason: 'No future round allocation configured',
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const remainingAllocation = tokenomics.futureRoundAllocationBps - (tokenomics.usedFutureRoundBps || 0);
|
|
1143
|
+
if (remainingAllocation <= 0) {
|
|
1144
|
+
return {
|
|
1145
|
+
canOpen: false,
|
|
1146
|
+
reason: 'Future round allocation pool exhausted',
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return {
|
|
1151
|
+
canOpen: true,
|
|
1152
|
+
nextRoundNumber: currentRound + 1,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Get remaining future round allocation in basis points
|
|
1158
|
+
*
|
|
1159
|
+
* @param tokenomics - Tokenomics account data
|
|
1160
|
+
* @returns Remaining allocation in BPS
|
|
1161
|
+
*/
|
|
1162
|
+
export function getRemainingFutureRoundAllocation(
|
|
1163
|
+
tokenomics: {
|
|
1164
|
+
futureRoundAllocationBps: number;
|
|
1165
|
+
usedFutureRoundBps?: number;
|
|
1166
|
+
}
|
|
1167
|
+
): number {
|
|
1168
|
+
return tokenomics.futureRoundAllocationBps - (tokenomics.usedFutureRoundBps || 0);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Convert Anchor round state object to string
|
|
1173
|
+
*/
|
|
1174
|
+
function getRoundStateString(state: unknown): string {
|
|
1175
|
+
if (typeof state === 'object' && state !== null) {
|
|
1176
|
+
const keys = Object.keys(state);
|
|
1177
|
+
return keys[0]?.toLowerCase() ?? 'unknown';
|
|
1178
|
+
}
|
|
1179
|
+
return 'unknown';
|
|
1180
|
+
}
|