@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.
Files changed (47) hide show
  1. package/README.md +11 -9
  2. package/dist/accounts/index.cjs +531 -3
  3. package/dist/accounts/index.cjs.map +1 -1
  4. package/dist/accounts/index.d.cts +307 -2
  5. package/dist/accounts/index.d.ts +307 -2
  6. package/dist/accounts/index.js +503 -4
  7. package/dist/accounts/index.js.map +1 -1
  8. package/dist/constants/index.cjs +41 -3
  9. package/dist/constants/index.cjs.map +1 -1
  10. package/dist/constants/index.d.cts +38 -3
  11. package/dist/constants/index.d.ts +38 -3
  12. package/dist/constants/index.js +40 -4
  13. package/dist/constants/index.js.map +1 -1
  14. package/dist/index.cjs +2297 -361
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +566 -7
  17. package/dist/index.d.ts +566 -7
  18. package/dist/index.js +2279 -379
  19. package/dist/index.js.map +1 -1
  20. package/dist/instructions/index.cjs +783 -40
  21. package/dist/instructions/index.cjs.map +1 -1
  22. package/dist/instructions/index.d.cts +492 -6
  23. package/dist/instructions/index.d.ts +492 -6
  24. package/dist/instructions/index.js +762 -42
  25. package/dist/instructions/index.js.map +1 -1
  26. package/dist/pdas/index.cjs +163 -1
  27. package/dist/pdas/index.cjs.map +1 -1
  28. package/dist/pdas/index.d.cts +131 -1
  29. package/dist/pdas/index.d.ts +131 -1
  30. package/dist/pdas/index.js +151 -2
  31. package/dist/pdas/index.js.map +1 -1
  32. package/dist/types/index.cjs +9 -0
  33. package/dist/types/index.cjs.map +1 -1
  34. package/dist/types/index.d.cts +586 -3
  35. package/dist/types/index.d.ts +586 -3
  36. package/dist/types/index.js +9 -1
  37. package/dist/types/index.js.map +1 -1
  38. package/package.json +5 -3
  39. package/src/__tests__/dynamic-tokenomics.test.ts +358 -0
  40. package/src/accounts/index.ts +852 -1
  41. package/src/client.ts +1130 -1
  42. package/src/constants/index.ts +48 -2
  43. package/src/index.ts +58 -0
  44. package/src/instructions/index.ts +1383 -40
  45. package/src/pdas/index.ts +346 -0
  46. package/src/types/index.ts +698 -2
  47. package/src/utils/index.ts +90 -0
package/src/client.ts CHANGED
@@ -12,7 +12,7 @@ import { Connection, PublicKey, Keypair } from '@solana/web3.js';
12
12
  import * as pdas from './pdas/index.js';
13
13
  import * as accounts from './accounts/index.js';
14
14
  import * as instructions from './instructions/index.js';
15
- import type { TierConfig } from './types/index.js';
15
+ import type { TierConfig, SubAllocation } from './types/index.js';
16
16
 
17
17
  /**
18
18
  * Wallet interface required by the client
@@ -158,6 +158,31 @@ export class RaiseClient {
158
158
  return pdas.getAdminConfigPDA(this.programId);
159
159
  }
160
160
 
161
+ getAllocationProposalPDA(projectPda: PublicKey, proposalIndex: number): PublicKey {
162
+ return pdas.getAllocationProposalPDA(projectPda, proposalIndex, this.programId);
163
+ }
164
+
165
+ getAllocationVotePDA(proposalPda: PublicKey, nftMint: PublicKey): PublicKey {
166
+ return pdas.getAllocationVotePDA(proposalPda, nftMint, this.programId);
167
+ }
168
+
169
+ getSubAllocationVestingPDA(projectPda: PublicKey, subAllocationId: number): PublicKey {
170
+ return pdas.getSubAllocationVestingPDA(projectPda, subAllocationId, this.programId);
171
+ }
172
+
173
+ getTokenomicsPDA(projectPda: PublicKey): PublicKey {
174
+ return pdas.getTokenomicsPDA(projectPda, this.programId);
175
+ }
176
+
177
+ // Per-Milestone Vesting PDAs
178
+ getInvestorMilestoneVestingPDA(projectPda: PublicKey, milestoneIndex: number, investmentPda: PublicKey): PublicKey {
179
+ return pdas.getInvestorMilestoneVestingPDA(projectPda, milestoneIndex, investmentPda, this.programId);
180
+ }
181
+
182
+ getFounderMilestoneVestingPDA(projectPda: PublicKey, milestoneIndex: number): PublicKey {
183
+ return pdas.getFounderMilestoneVestingPDA(projectPda, milestoneIndex, this.programId);
184
+ }
185
+
161
186
  // ===========================================================================
162
187
  // Account Fetchers
163
188
  // ===========================================================================
@@ -202,6 +227,38 @@ export class RaiseClient {
202
227
  return accounts.fetchAdminConfig(this.program);
203
228
  }
204
229
 
230
+ async fetchTokenVault(projectId: BN) {
231
+ return accounts.fetchTokenVault(this.program, projectId);
232
+ }
233
+
234
+ async fetchTokenomics(projectId: BN) {
235
+ return accounts.fetchTokenomics(this.program, projectId);
236
+ }
237
+
238
+ async fetchAllocationProposal(projectId: BN, proposalIndex: number) {
239
+ return accounts.fetchAllocationProposal(this.program, projectId, proposalIndex);
240
+ }
241
+
242
+ async fetchAllAllocationProposals(projectId: BN) {
243
+ return accounts.fetchAllAllocationProposals(this.program, projectId);
244
+ }
245
+
246
+ async fetchAllocationVote(projectId: BN, proposalIndex: number, nftMint: PublicKey) {
247
+ return accounts.fetchAllocationVote(this.program, projectId, proposalIndex, nftMint);
248
+ }
249
+
250
+ async fetchSubAllocationVesting(projectId: BN, subAllocationId: number) {
251
+ return accounts.fetchSubAllocationVesting(this.program, projectId, subAllocationId);
252
+ }
253
+
254
+ async fetchInvestorMilestoneVesting(projectId: BN, milestoneIndex: number, nftMint: PublicKey) {
255
+ return accounts.fetchInvestorMilestoneVesting(this.program, projectId, milestoneIndex, nftMint);
256
+ }
257
+
258
+ async fetchFounderMilestoneVesting(projectId: BN, milestoneIndex: number) {
259
+ return accounts.fetchFounderMilestoneVesting(this.program, projectId, milestoneIndex);
260
+ }
261
+
205
262
  // ===========================================================================
206
263
  // Admin Instructions
207
264
  // ===========================================================================
@@ -242,6 +299,8 @@ export class RaiseClient {
242
299
  tokenomics: instructions.TokenomicsInput;
243
300
  /** Milestone 1 deadline - Unix timestamp (required) */
244
301
  milestone1Deadline: BN;
302
+ /** Milestone Price Increases: Price multipliers in BPS (optional, defaults to all 10000) */
303
+ priceMultipliers?: number[];
245
304
  }): Promise<string> {
246
305
  return instructions.initializeProject(
247
306
  this.program,
@@ -278,6 +337,9 @@ export class RaiseClient {
278
337
  milestoneIndex: number;
279
338
  percentage: number;
280
339
  description: string;
340
+ vestingDurationMonths?: number | null;
341
+ cliffMonths?: number | null;
342
+ instantReleaseBps?: number | null;
281
343
  }): Promise<string> {
282
344
  return instructions.createMilestone(
283
345
  this.program,
@@ -686,6 +748,170 @@ export class RaiseClient {
686
748
  );
687
749
  }
688
750
 
751
+ // ===========================================================================
752
+ // Early Token Release Instructions
753
+ // ===========================================================================
754
+
755
+ /**
756
+ * Claim early tokens as an investor (5% of token allocation)
757
+ *
758
+ * Early Token Release: Investors can claim 5% of their token allocation
759
+ * 24 hours after investing. These tokens are fully liquid immediately.
760
+ *
761
+ * Prerequisites:
762
+ * - 24h cooling period must have passed since investment
763
+ * - Investment must not have already claimed early tokens
764
+ * - Investment must have active voting rights
765
+ */
766
+ async claimEarlyTokens(args: {
767
+ projectId: BN;
768
+ nftMint: PublicKey;
769
+ investorTokenAccount: PublicKey;
770
+ }): Promise<string> {
771
+ return instructions.claimEarlyTokens(
772
+ this.program,
773
+ args,
774
+ this.walletPublicKey
775
+ );
776
+ }
777
+
778
+ /**
779
+ * Claim early tokens as a founder (5% of founder allocation)
780
+ *
781
+ * Early Token Release: Founders can claim 5% of their token allocation
782
+ * when the project becomes Funded. These tokens are fully liquid immediately.
783
+ *
784
+ * Prerequisites:
785
+ * - Project must be in Funded, InProgress, or Completed state
786
+ * - Founder must not have already claimed early tokens
787
+ */
788
+ async claimFounderEarlyTokens(args: {
789
+ projectId: BN;
790
+ founderTokenAccount: PublicKey;
791
+ }): Promise<string> {
792
+ return instructions.claimFounderEarlyTokens(
793
+ this.program,
794
+ args,
795
+ this.walletPublicKey
796
+ );
797
+ }
798
+
799
+ /**
800
+ * Claim founder milestone-based tokens for a specific milestone
801
+ *
802
+ * Early Token Release: Founders can claim their milestone-based portion
803
+ * (default 47.5% of founder allocation) when milestones are unlocked.
804
+ *
805
+ * Prerequisites:
806
+ * - Milestone must be in Unlocked state
807
+ * - Founder must not have already claimed tokens for this milestone
808
+ */
809
+ async claimFounderMilestoneTokens(args: {
810
+ projectId: BN;
811
+ milestoneIndex: number;
812
+ founderTokenAccount: PublicKey;
813
+ }): Promise<string> {
814
+ return instructions.claimFounderMilestoneTokens(
815
+ this.program,
816
+ args,
817
+ this.walletPublicKey
818
+ );
819
+ }
820
+
821
+ // ===========================================================================
822
+ // Per-Milestone Vesting Instructions (add-per-milestone-vesting)
823
+ // ===========================================================================
824
+
825
+ /**
826
+ * Claim instant tokens for a passed milestone (investor)
827
+ *
828
+ * Per-milestone vesting: Investors claim the instant portion (e.g., 5-50%)
829
+ * immediately when a milestone passes. Creates vesting PDA on first claim.
830
+ *
831
+ * @param milestoneIndex - The milestone index to claim tokens from
832
+ * @param nftMint - The NFT mint proving investment ownership
833
+ * @param investorTokenAccount - Investor's token account to receive claimed tokens
834
+ */
835
+ async claimMilestoneInstantTokens(args: {
836
+ projectId: BN;
837
+ milestoneIndex: number;
838
+ nftMint: PublicKey;
839
+ investorTokenAccount: PublicKey;
840
+ }): Promise<string> {
841
+ return instructions.claimMilestoneInstantTokens(
842
+ this.program,
843
+ args,
844
+ this.walletPublicKey
845
+ );
846
+ }
847
+
848
+ /**
849
+ * Claim vested tokens for a milestone after cliff period (investor)
850
+ *
851
+ * Per-milestone vesting: Investors claim vested tokens linearly after
852
+ * the cliff period passes. Must have already claimed instant tokens
853
+ * to initialize the vesting PDA.
854
+ *
855
+ * @param milestoneIndex - The milestone index to claim vested tokens from
856
+ * @param nftMint - The NFT mint proving investment ownership
857
+ * @param investorTokenAccount - Investor's token account to receive claimed tokens
858
+ */
859
+ async claimMilestoneVestedTokens(args: {
860
+ projectId: BN;
861
+ milestoneIndex: number;
862
+ nftMint: PublicKey;
863
+ investorTokenAccount: PublicKey;
864
+ }): Promise<string> {
865
+ return instructions.claimMilestoneVestedTokens(
866
+ this.program,
867
+ args,
868
+ this.walletPublicKey
869
+ );
870
+ }
871
+
872
+ /**
873
+ * Claim instant tokens for a passed milestone (founder)
874
+ *
875
+ * Per-milestone vesting: Founders claim the instant portion (e.g., 5-50%)
876
+ * of their milestone-based allocation when a milestone passes.
877
+ *
878
+ * @param milestoneIndex - The milestone index to claim tokens from
879
+ * @param founderTokenAccount - Founder's token account to receive claimed tokens
880
+ */
881
+ async claimFounderMsInstantTokens(args: {
882
+ projectId: BN;
883
+ milestoneIndex: number;
884
+ founderTokenAccount: PublicKey;
885
+ }): Promise<string> {
886
+ return instructions.claimFounderMsInstantTokens(
887
+ this.program,
888
+ args,
889
+ this.walletPublicKey
890
+ );
891
+ }
892
+
893
+ /**
894
+ * Claim vested tokens for a milestone after cliff period (founder)
895
+ *
896
+ * Per-milestone vesting: Founders claim vested tokens linearly after
897
+ * the cliff period passes. Must have already claimed instant tokens
898
+ * to initialize the vesting PDA.
899
+ *
900
+ * @param milestoneIndex - The milestone index to claim vested tokens from
901
+ * @param founderTokenAccount - Founder's token account to receive claimed tokens
902
+ */
903
+ async claimFounderMsVestedTokens(args: {
904
+ projectId: BN;
905
+ milestoneIndex: number;
906
+ founderTokenAccount: PublicKey;
907
+ }): Promise<string> {
908
+ return instructions.claimFounderMsVestedTokens(
909
+ this.program,
910
+ args,
911
+ this.walletPublicKey
912
+ );
913
+ }
914
+
689
915
  // ===========================================================================
690
916
  // Abandonment Instructions
691
917
  // ===========================================================================
@@ -712,4 +938,907 @@ export class RaiseClient {
712
938
  this.walletPublicKey
713
939
  );
714
940
  }
941
+
942
+ // ===========================================================================
943
+ // Sub-Allocation Vesting Instructions
944
+ // ===========================================================================
945
+
946
+ /**
947
+ * Initialize sub-allocation vesting after MAE (Market Access Event)
948
+ *
949
+ * Creates SubAllocationVesting PDA for a specific sub-allocation ID.
950
+ * Project must be in Completed state with MAE completed.
951
+ * Permissionless - anyone can pay to initialize.
952
+ */
953
+ async initializeSubAllocationVesting(args: {
954
+ projectId: BN;
955
+ subAllocationId: number;
956
+ }): Promise<string> {
957
+ return instructions.initializeSubAllocationVesting(
958
+ this.program,
959
+ args,
960
+ this.walletPublicKey
961
+ );
962
+ }
963
+
964
+ /**
965
+ * Claim vested tokens from a sub-allocation
966
+ *
967
+ * Only the designated recipient wallet can claim tokens.
968
+ * Project must be Completed and MAE must be completed.
969
+ */
970
+ async claimSubAllocationTokens(args: {
971
+ projectId: BN;
972
+ subAllocationId: number;
973
+ recipientTokenAccount: PublicKey;
974
+ }): Promise<string> {
975
+ return instructions.claimSubAllocationTokens(
976
+ this.program,
977
+ args,
978
+ this.walletPublicKey
979
+ );
980
+ }
981
+
982
+ // ===========================================================================
983
+ // Dynamic Tokenomics Instructions
984
+ // ===========================================================================
985
+
986
+ /**
987
+ * Add a sub-allocation from the reserve pool (Draft state only)
988
+ *
989
+ * Allows founders to add named allocations (advisors, marketing, etc.)
990
+ * from their reserve pool before submitting for approval.
991
+ */
992
+ async addSubAllocation(args: {
993
+ projectId: BN;
994
+ name: string;
995
+ bps: number;
996
+ recipient: PublicKey;
997
+ vestingMonths: number;
998
+ cliffMonths: number;
999
+ }): Promise<string> {
1000
+ return instructions.addSubAllocation(
1001
+ this.program,
1002
+ args,
1003
+ this.walletPublicKey
1004
+ );
1005
+ }
1006
+
1007
+ /**
1008
+ * Propose an allocation change via governance (post-Draft states)
1009
+ *
1010
+ * Creates a 7-day voting period for investors to approve/reject
1011
+ * a new sub-allocation from the reserve pool.
1012
+ */
1013
+ async proposeAllocationChange(args: {
1014
+ projectId: BN;
1015
+ name: string;
1016
+ bps: number;
1017
+ recipient: PublicKey;
1018
+ vestingMonths: number;
1019
+ cliffMonths: number;
1020
+ }): Promise<string> {
1021
+ return instructions.proposeAllocationChange(
1022
+ this.program,
1023
+ args,
1024
+ this.walletPublicKey
1025
+ );
1026
+ }
1027
+
1028
+ /**
1029
+ * Vote on an allocation change proposal
1030
+ *
1031
+ * Investors can vote for/against using their Investment NFT.
1032
+ * Must meet 7-day hold period for voting eligibility.
1033
+ */
1034
+ async voteAllocationChange(args: {
1035
+ projectId: BN;
1036
+ proposalIndex: number;
1037
+ nftMint: PublicKey;
1038
+ voteFor: boolean;
1039
+ }): Promise<string> {
1040
+ return instructions.voteAllocationChange(
1041
+ this.program,
1042
+ args,
1043
+ this.walletPublicKey
1044
+ );
1045
+ }
1046
+
1047
+ /**
1048
+ * Execute an approved allocation change proposal
1049
+ *
1050
+ * Permissionless - anyone can call after voting ends.
1051
+ * Proposal must have passed (>51% approval).
1052
+ */
1053
+ async executeAllocationChange(args: {
1054
+ projectId: BN;
1055
+ proposalIndex: number;
1056
+ }): Promise<string> {
1057
+ return instructions.executeAllocationChange(
1058
+ this.program,
1059
+ args,
1060
+ this.walletPublicKey
1061
+ );
1062
+ }
1063
+
1064
+ // ===========================================================================
1065
+ // Dynamic Tokenomics Helpers
1066
+ // ===========================================================================
1067
+
1068
+ /**
1069
+ * Get remaining founder pool capacity for sub-allocations in basis points
1070
+ *
1071
+ * Calculates: founder_allocation_bps - sum(sub_allocations.bps)
1072
+ * Sub-allocations (treasury, advisors, marketing) draw from founder's allocation pool.
1073
+ */
1074
+ async getRemainingFounderPool(projectId: BN): Promise<number> {
1075
+ const tokenomics = await this.fetchTokenomics(projectId);
1076
+ if (!tokenomics) {
1077
+ throw new Error('Tokenomics account not found');
1078
+ }
1079
+
1080
+ const founderBps = tokenomics.founderAllocationBps || 0;
1081
+ const usedBps = (tokenomics.subAllocations || [])
1082
+ .filter((sa: SubAllocation) => sa.bps > 0)
1083
+ .reduce((sum: number, sa: SubAllocation) => sum + sa.bps, 0);
1084
+
1085
+ return founderBps - usedBps;
1086
+ }
1087
+
1088
+ /**
1089
+ * Get all active (non-executed, non-expired) allocation proposals
1090
+ */
1091
+ async getActiveProposals(projectId: BN) {
1092
+ const proposals = await this.fetchAllAllocationProposals(projectId);
1093
+ const now = Math.floor(Date.now() / 1000);
1094
+
1095
+ return proposals.filter((p) => {
1096
+ const account = p.account;
1097
+ // Filter for non-executed proposals that haven't expired
1098
+ return !account.executed && account.votingEndsAt.toNumber() > now;
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * Get vesting status for a sub-allocation
1104
+ *
1105
+ * Returns claimable amount, total vested, and remaining locked tokens.
1106
+ */
1107
+ async getSubAllocationVestingStatus(projectId: BN, subAllocationId: number) {
1108
+ const vesting = await this.fetchSubAllocationVesting(projectId, subAllocationId);
1109
+ if (!vesting) {
1110
+ return null;
1111
+ }
1112
+
1113
+ const now = Math.floor(Date.now() / 1000);
1114
+ // Use the pre-computed timestamps from the on-chain account
1115
+ const cliffEnd = vesting.cliffEnd.toNumber();
1116
+ const vestingEnd = vesting.vestingEnd.toNumber();
1117
+ const totalAmount = vesting.totalAmount.toNumber();
1118
+ const claimedAmount = vesting.claimedAmount.toNumber();
1119
+
1120
+ // Before cliff - nothing vested
1121
+ if (now < cliffEnd) {
1122
+ return {
1123
+ totalAmount,
1124
+ vestedAmount: 0,
1125
+ claimedAmount,
1126
+ claimableAmount: 0,
1127
+ lockedAmount: totalAmount - claimedAmount,
1128
+ vestingProgress: 0,
1129
+ cliffPassed: false,
1130
+ };
1131
+ }
1132
+
1133
+ // After full vesting period - everything vested
1134
+ if (now >= vestingEnd) {
1135
+ return {
1136
+ totalAmount,
1137
+ vestedAmount: totalAmount,
1138
+ claimedAmount,
1139
+ claimableAmount: totalAmount - claimedAmount,
1140
+ lockedAmount: 0,
1141
+ vestingProgress: 100,
1142
+ cliffPassed: true,
1143
+ };
1144
+ }
1145
+
1146
+ // Linear vesting calculation
1147
+ const vestingDuration = vestingEnd - cliffEnd;
1148
+ const elapsed = now - cliffEnd;
1149
+ const vestedAmount = Math.floor((totalAmount * elapsed) / vestingDuration);
1150
+
1151
+ return {
1152
+ totalAmount,
1153
+ vestedAmount,
1154
+ claimedAmount,
1155
+ claimableAmount: vestedAmount - claimedAmount,
1156
+ lockedAmount: totalAmount - vestedAmount,
1157
+ vestingProgress: Math.floor((elapsed / vestingDuration) * 100),
1158
+ cliffPassed: true,
1159
+ };
1160
+ }
1161
+
1162
+ // ===========================================================================
1163
+ // Milestone Price Increase Helpers
1164
+ // ===========================================================================
1165
+
1166
+ /** Base price multiplier constant (10000 BPS = 1.0x) */
1167
+ static readonly PRICE_MULTIPLIER_BASE = 10000;
1168
+
1169
+ /**
1170
+ * Get the current price multiplier for a project
1171
+ *
1172
+ * Returns the multiplier in BPS (10000 = 1.0x, 15000 = 1.5x)
1173
+ *
1174
+ * Supports two modes:
1175
+ * - Dynamic mode (priceMultipliers[0] === 0): Uses currentPriceMultiplierBps
1176
+ * calculated dynamically via ZEMYTH formula at each milestone claim
1177
+ * - Static mode (legacy): Uses pre-configured priceMultipliers array lookup
1178
+ */
1179
+ async getCurrentMultiplier(projectId: BN): Promise<number> {
1180
+ const project = await this.fetchProject(projectId);
1181
+ if (!project) {
1182
+ throw new Error('Project account not found');
1183
+ }
1184
+
1185
+ const { priceMultipliers, milestonesPassed, milestoneCount, currentPriceMultiplierBps } = project;
1186
+
1187
+ // Check for dynamic mode sentinel (priceMultipliers[0] === 0)
1188
+ if (priceMultipliers[0] === 0) {
1189
+ // Dynamic mode: return the dynamically calculated multiplier
1190
+ return currentPriceMultiplierBps || RaiseClient.PRICE_MULTIPLIER_BASE;
1191
+ }
1192
+
1193
+ // Static mode (legacy): use array lookup
1194
+ // Clamp index to valid range (0 to milestoneCount - 1)
1195
+ const index = milestoneCount === 0
1196
+ ? 0
1197
+ : Math.min(milestonesPassed, milestoneCount - 1);
1198
+
1199
+ return priceMultipliers[index] || RaiseClient.PRICE_MULTIPLIER_BASE;
1200
+ }
1201
+
1202
+ /**
1203
+ * Check if a project uses dynamic price multiplier mode
1204
+ */
1205
+ async isDynamicMultiplierMode(projectId: BN): Promise<boolean> {
1206
+ const project = await this.fetchProject(projectId);
1207
+ if (!project) {
1208
+ throw new Error('Project account not found');
1209
+ }
1210
+
1211
+ return project.priceMultipliers[0] === 0;
1212
+ }
1213
+
1214
+ /**
1215
+ * Calculate effective tier prices with current multiplier
1216
+ *
1217
+ * Returns array of tiers with their effective prices after applying
1218
+ * the current milestone multiplier.
1219
+ */
1220
+ async getEffectiveTierPrices(projectId: BN): Promise<Array<{
1221
+ tierIndex: number;
1222
+ baseAmount: BN;
1223
+ effectiveAmount: BN;
1224
+ multiplierBps: number;
1225
+ }>> {
1226
+ const project = await this.fetchProject(projectId);
1227
+ if (!project) {
1228
+ throw new Error('Project account not found');
1229
+ }
1230
+
1231
+ const multiplier = await this.getCurrentMultiplier(projectId);
1232
+ const tiers = project.tiers.slice(0, project.tierCount);
1233
+
1234
+ return tiers.map((tier: { amount: BN }, index: number) => ({
1235
+ tierIndex: index,
1236
+ baseAmount: tier.amount,
1237
+ effectiveAmount: tier.amount
1238
+ .mul(new BN(multiplier))
1239
+ .div(new BN(RaiseClient.PRICE_MULTIPLIER_BASE)),
1240
+ multiplierBps: multiplier,
1241
+ }));
1242
+ }
1243
+
1244
+ /**
1245
+ * Get price schedule showing multipliers after each milestone
1246
+ *
1247
+ * Returns array showing what the price multiplier will be
1248
+ * after each milestone passes.
1249
+ *
1250
+ * For dynamic mode projects (priceMultipliers[0] === 0), returns null
1251
+ * as multipliers are calculated dynamically based on deadline timing.
1252
+ */
1253
+ async getPriceSchedule(projectId: BN): Promise<Array<{
1254
+ afterMilestone: number;
1255
+ multiplierBps: number;
1256
+ multiplierPercent: number;
1257
+ }> | null> {
1258
+ const project = await this.fetchProject(projectId);
1259
+ if (!project) {
1260
+ throw new Error('Project account not found');
1261
+ }
1262
+
1263
+ const { priceMultipliers, milestoneCount } = project;
1264
+
1265
+ // Dynamic mode: no pre-set schedule
1266
+ if (priceMultipliers[0] === 0) {
1267
+ return null;
1268
+ }
1269
+
1270
+ // Static mode: return pre-configured schedule
1271
+ return priceMultipliers
1272
+ .slice(0, milestoneCount)
1273
+ .map((multiplier: number, index: number) => ({
1274
+ afterMilestone: index,
1275
+ multiplierBps: multiplier,
1276
+ multiplierPercent: multiplier / 100, // 10000 -> 100%
1277
+ }));
1278
+ }
1279
+
1280
+ /**
1281
+ * Preview multiplier calculation for dynamic mode projects
1282
+ *
1283
+ * Uses the ZEMYTH formula to calculate what the multiplier would be
1284
+ * given a specific number of remaining milestones and days to deadline.
1285
+ *
1286
+ * Formula: totalMultiplier = 1 + (sqrt(n) / sqrt(15)) × (1 / ln(d/7 + e))
1287
+ *
1288
+ * @param remainingMilestones Number of milestones left (1-10)
1289
+ * @param daysToDeadline Days until next milestone deadline (1-365)
1290
+ * @returns Preview of the multiplier in BPS (10000 = 1.0x, max 20000 = 2.0x)
1291
+ */
1292
+ previewDynamicMultiplier(remainingMilestones: number, daysToDeadline: number): number {
1293
+ // Edge case: 0 remaining milestones = base multiplier
1294
+ if (remainingMilestones === 0) {
1295
+ return RaiseClient.PRICE_MULTIPLIER_BASE;
1296
+ }
1297
+
1298
+ // Clamp days to reasonable range
1299
+ const days = Math.max(1, Math.min(365, daysToDeadline));
1300
+
1301
+ // Integer approximation of ZEMYTH formula
1302
+ // sqrt(n) / sqrt(15) ≈ (sqrt(n) * 1000) / 3873
1303
+ const sqrtN = Math.sqrt(remainingMilestones) * 1000;
1304
+ const milestoneFactor = sqrtN / 3.873;
1305
+
1306
+ // 1/ln(d/7 + e) approximation
1307
+ const timeFactor = this.timeFactorLookup(days);
1308
+
1309
+ // Combined factor
1310
+ const combined = (milestoneFactor * timeFactor) / 1000;
1311
+ const bonusBps = Math.min(combined, 10000);
1312
+
1313
+ // Base + bonus, clamped to [10000, 20000]
1314
+ return Math.min(20000, Math.max(10000, Math.round(10000 + bonusBps)));
1315
+ }
1316
+
1317
+ /** Time factor lookup for dynamic multiplier preview (approximates 1/ln(d/7 + e) × 1000) */
1318
+ private timeFactorLookup(days: number): number {
1319
+ if (days <= 7) return 950 - (days * 25);
1320
+ if (days <= 14) return 775 - ((days - 7) * 20);
1321
+ if (days <= 30) return 635 - ((days - 14) * 8);
1322
+ if (days <= 60) return 507 - ((days - 30) * 4);
1323
+ if (days <= 90) return 387 - ((days - 60) * 3);
1324
+ if (days <= 180) return 297 - ((days - 90) * 1);
1325
+ return 250;
1326
+ }
1327
+
1328
+ // ===========================================================================
1329
+ // Linear Progressive Pricing Helpers (add-linear-progressive-pricing)
1330
+ // ===========================================================================
1331
+
1332
+ /** Constants for progressive pricing */
1333
+ static readonly TIME_ALLOCATION_PERCENT = 50;
1334
+ static readonly MILESTONE_ALLOCATION_PERCENT = 50;
1335
+ /** DEV MODE: Time acceleration multiplier (10 real seconds = 1 simulated day) */
1336
+ static readonly PROGRESSIVE_TIME_MULTIPLIER = 8640;
1337
+
1338
+ /**
1339
+ * Check if progressive pricing is active for a project
1340
+ *
1341
+ * Progressive pricing is active when milestonePeriodStartedAt > 0,
1342
+ * meaning the first milestone period has been initialized.
1343
+ */
1344
+ isProgressivePricingActive(project: { milestonePeriodStartedAt: BN }): boolean {
1345
+ return project.milestonePeriodStartedAt.gt(new BN(0));
1346
+ }
1347
+
1348
+ /**
1349
+ * Calculate time progress for a milestone period
1350
+ *
1351
+ * @param project Project account with progressive pricing fields
1352
+ * @param currentTime Current unix timestamp (seconds)
1353
+ * @returns Progress as ratio 0-1 (capped at 1.0)
1354
+ */
1355
+ getTimeProgress(
1356
+ project: {
1357
+ milestonePeriodStartedAt: BN;
1358
+ expectedMilestoneDeadlineTs: BN;
1359
+ },
1360
+ currentTime: number
1361
+ ): number {
1362
+ const startedAt = project.milestonePeriodStartedAt.toNumber();
1363
+ const deadline = project.expectedMilestoneDeadlineTs.toNumber();
1364
+
1365
+ if (startedAt === 0 || deadline <= startedAt) {
1366
+ return 0;
1367
+ }
1368
+
1369
+ // DEV MODE: Apply time acceleration (10 real seconds = 1 simulated day)
1370
+ const rawElapsed = Math.max(0, currentTime - startedAt);
1371
+ const elapsed = rawElapsed * RaiseClient.PROGRESSIVE_TIME_MULTIPLIER;
1372
+ const expectedDuration = Math.max(1, deadline - startedAt);
1373
+
1374
+ return Math.min(1, elapsed / expectedDuration);
1375
+ }
1376
+
1377
+ /**
1378
+ * Calculate the effective multiplier including time-based component
1379
+ *
1380
+ * @param project Project account with progressive pricing fields
1381
+ * @param currentTime Current unix timestamp (seconds)
1382
+ * @returns Effective multiplier in BPS (10000 = 1.0x)
1383
+ */
1384
+ calculateEffectiveMultiplier(
1385
+ project: {
1386
+ milestonePeriodStartedAt: BN;
1387
+ expectedMilestoneDeadlineTs: BN;
1388
+ milestoneBaseMultiplierBps: number;
1389
+ timeAllocationBps: number;
1390
+ currentPriceMultiplierBps: number;
1391
+ priceMultipliers: number[];
1392
+ },
1393
+ currentTime: number
1394
+ ): number {
1395
+ // If progressive pricing not active, use legacy behavior
1396
+ if (!this.isProgressivePricingActive(project)) {
1397
+ // Legacy: use currentPriceMultiplierBps or static array
1398
+ if (project.priceMultipliers[0] === 0) {
1399
+ return project.currentPriceMultiplierBps || RaiseClient.PRICE_MULTIPLIER_BASE;
1400
+ }
1401
+ return project.currentPriceMultiplierBps || RaiseClient.PRICE_MULTIPLIER_BASE;
1402
+ }
1403
+
1404
+ // Progressive pricing active: base + time increment
1405
+ const timeProgress = this.getTimeProgress(project, currentTime);
1406
+ const timeIncrement = Math.floor(project.timeAllocationBps * timeProgress);
1407
+
1408
+ return project.milestoneBaseMultiplierBps + timeIncrement;
1409
+ }
1410
+
1411
+ /**
1412
+ * Preview what the milestone spike will be at a given claim time
1413
+ *
1414
+ * If founder claims early, unused time allocation accumulates to spike.
1415
+ *
1416
+ * @param project Project account with progressive pricing fields
1417
+ * @param claimTime Unix timestamp when claim will happen
1418
+ * @returns Object with spike details
1419
+ */
1420
+ previewMilestoneSpike(
1421
+ project: {
1422
+ milestonePeriodStartedAt: BN;
1423
+ expectedMilestoneDeadlineTs: BN;
1424
+ milestoneBaseMultiplierBps: number;
1425
+ timeAllocationBps: number;
1426
+ milestoneAllocationBps: number;
1427
+ },
1428
+ claimTime: number
1429
+ ): {
1430
+ timeProgress: number;
1431
+ consumedTimeBps: number;
1432
+ unusedTimeBps: number;
1433
+ milestoneSpikeBps: number;
1434
+ effectiveMultiplierBps: number;
1435
+ newBaseBps: number;
1436
+ } {
1437
+ const timeProgress = this.getTimeProgress(project, claimTime);
1438
+
1439
+ const consumedTimeBps = Math.floor(project.timeAllocationBps * timeProgress);
1440
+ const unusedTimeBps = project.timeAllocationBps - consumedTimeBps;
1441
+ const milestoneSpikeBps = project.milestoneAllocationBps + unusedTimeBps;
1442
+
1443
+ const effectiveMultiplierBps = project.milestoneBaseMultiplierBps + consumedTimeBps;
1444
+ const newBaseBps = effectiveMultiplierBps + milestoneSpikeBps;
1445
+
1446
+ return {
1447
+ timeProgress,
1448
+ consumedTimeBps,
1449
+ unusedTimeBps,
1450
+ milestoneSpikeBps,
1451
+ effectiveMultiplierBps,
1452
+ newBaseBps,
1453
+ };
1454
+ }
1455
+
1456
+ /**
1457
+ * Get effective tier prices with time-based progressive pricing
1458
+ *
1459
+ * Unlike getEffectiveTierPrices which fetches the project, this takes
1460
+ * the project and current time to calculate prices including time component.
1461
+ */
1462
+ getProgressiveTierPrices(
1463
+ project: {
1464
+ tiers: Array<{ amount: BN }>;
1465
+ tierCount: number;
1466
+ milestonePeriodStartedAt: BN;
1467
+ expectedMilestoneDeadlineTs: BN;
1468
+ milestoneBaseMultiplierBps: number;
1469
+ timeAllocationBps: number;
1470
+ currentPriceMultiplierBps: number;
1471
+ priceMultipliers: number[];
1472
+ },
1473
+ currentTime: number
1474
+ ): Array<{
1475
+ tierIndex: number;
1476
+ baseAmount: BN;
1477
+ effectiveAmount: BN;
1478
+ multiplierBps: number;
1479
+ timeProgressPercent: number;
1480
+ }> {
1481
+ const multiplier = this.calculateEffectiveMultiplier(project, currentTime);
1482
+ const timeProgress = this.getTimeProgress(project, currentTime);
1483
+ const tiers = project.tiers.slice(0, project.tierCount);
1484
+
1485
+ return tiers.map((tier, index) => ({
1486
+ tierIndex: index,
1487
+ baseAmount: tier.amount,
1488
+ effectiveAmount: tier.amount
1489
+ .mul(new BN(multiplier))
1490
+ .div(new BN(RaiseClient.PRICE_MULTIPLIER_BASE)),
1491
+ multiplierBps: multiplier,
1492
+ timeProgressPercent: Math.round(timeProgress * 100),
1493
+ }));
1494
+ }
1495
+
1496
+ // ===========================================================================
1497
+ // Multi-Round Fundraising PDAs
1498
+ // ===========================================================================
1499
+
1500
+ getFundingRoundPDA(projectPda: PublicKey, roundNumber: number): PublicKey {
1501
+ return pdas.getFundingRoundPDA(projectPda, roundNumber, this.programId);
1502
+ }
1503
+
1504
+ getRoundEscrowPDA(projectPda: PublicKey, roundNumber: number): PublicKey {
1505
+ return pdas.getRoundEscrowPDA(projectPda, roundNumber, this.programId);
1506
+ }
1507
+
1508
+ getRoundMilestonePDA(projectPda: PublicKey, roundNumber: number, milestoneIndex: number): PublicKey {
1509
+ return pdas.getRoundMilestonePDA(projectPda, roundNumber, milestoneIndex, this.programId);
1510
+ }
1511
+
1512
+ getRoundInvestmentPDA(projectPda: PublicKey, roundNumber: number, nftMint: PublicKey): PublicKey {
1513
+ return pdas.getRoundInvestmentPDA(projectPda, roundNumber, nftMint, this.programId);
1514
+ }
1515
+
1516
+ getRoundInvestorMilestoneVestingPDA(
1517
+ projectPda: PublicKey,
1518
+ roundNumber: number,
1519
+ milestoneIndex: number,
1520
+ investmentPda: PublicKey
1521
+ ): PublicKey {
1522
+ return pdas.getRoundInvestorMilestoneVestingPDA(
1523
+ projectPda,
1524
+ roundNumber,
1525
+ milestoneIndex,
1526
+ investmentPda,
1527
+ this.programId
1528
+ );
1529
+ }
1530
+
1531
+ getFutureRoundVaultPDA(projectPda: PublicKey): PublicKey {
1532
+ return pdas.getFutureRoundVaultPDA(projectPda, this.programId);
1533
+ }
1534
+
1535
+ getFutureRoundTokenVaultPDA(projectPda: PublicKey): PublicKey {
1536
+ return pdas.getFutureRoundTokenVaultPDA(projectPda, this.programId);
1537
+ }
1538
+
1539
+ // ===========================================================================
1540
+ // Multi-Round Fundraising Account Fetchers
1541
+ // ===========================================================================
1542
+
1543
+ async fetchFundingRound(projectId: BN, roundNumber: number) {
1544
+ return accounts.fetchFundingRound(this.program, projectId, roundNumber);
1545
+ }
1546
+
1547
+ async fetchAllFundingRounds(projectId: BN) {
1548
+ return accounts.fetchAllFundingRounds(this.program, projectId);
1549
+ }
1550
+
1551
+ async fetchRoundMilestone(projectId: BN, roundNumber: number, milestoneIndex: number) {
1552
+ return accounts.fetchRoundMilestone(this.program, projectId, roundNumber, milestoneIndex);
1553
+ }
1554
+
1555
+ async fetchRoundInvestment(projectId: BN, roundNumber: number, nftMint: PublicKey) {
1556
+ return accounts.fetchRoundInvestment(this.program, projectId, roundNumber, nftMint);
1557
+ }
1558
+
1559
+ async fetchFutureRoundVault(projectId: BN) {
1560
+ return accounts.fetchFutureRoundVault(this.program, projectId);
1561
+ }
1562
+
1563
+ // ===========================================================================
1564
+ // Multi-Round Fundraising Instructions
1565
+ // ===========================================================================
1566
+
1567
+ /**
1568
+ * Open a new funding round (R2, R3, R4...)
1569
+ *
1570
+ * Multi-Round Fundraising: Creates FundingRound PDA, round escrow,
1571
+ * and milestone PDAs for the new round. Tokens come from FutureRoundVault.
1572
+ *
1573
+ * Prerequisites:
1574
+ * - Project must be InProgress or Completed state
1575
+ * - At least 1 milestone must have passed (for InProgress projects)
1576
+ * - No other round currently Open
1577
+ * - Current round must be fully funded before opening next
1578
+ * - Must have future_round_allocation configured in tokenomics
1579
+ */
1580
+ async openFundingRound(args: {
1581
+ projectId: BN;
1582
+ roundAllocationBps: number;
1583
+ fundingGoal: BN;
1584
+ tiers: Array<{ amount: BN; maxLots: number; tokenRatio: BN }>;
1585
+ milestones: Array<{
1586
+ percentage: number;
1587
+ description: string;
1588
+ vestingDurationMonths?: number | null;
1589
+ cliffMonths?: number | null;
1590
+ instantReleaseBps?: number | null;
1591
+ }>;
1592
+ usdcMint: PublicKey;
1593
+ previousFundingRoundPda?: PublicKey | null;
1594
+ }): Promise<string> {
1595
+ return instructions.openFundingRound(
1596
+ this.program,
1597
+ args,
1598
+ this.walletPublicKey
1599
+ );
1600
+ }
1601
+
1602
+ /**
1603
+ * Invest in a funding round (R2, R3, R4...)
1604
+ *
1605
+ * Multi-Round Fundraising: Creates investment NFT and deposits USDC
1606
+ * to the round-specific escrow. Tokens come from FutureRoundVault.
1607
+ */
1608
+ async investInRound(args: {
1609
+ projectId: BN;
1610
+ roundNumber: number;
1611
+ amount: BN;
1612
+ investorTokenAccount: PublicKey;
1613
+ investmentCount: number;
1614
+ }): Promise<string> {
1615
+ return instructions.investInRound(
1616
+ this.program,
1617
+ args,
1618
+ this.walletPublicKey
1619
+ );
1620
+ }
1621
+
1622
+ /**
1623
+ * Cancel round investment within 24-hour cooling-off period
1624
+ *
1625
+ * Multi-Round Fundraising: Returns USDC from round escrow,
1626
+ * closes investment account.
1627
+ */
1628
+ async cancelRoundInvestment(args: {
1629
+ projectId: BN;
1630
+ roundNumber: number;
1631
+ nftMint: PublicKey;
1632
+ investorNftAccount: PublicKey;
1633
+ investorUsdcAccount: PublicKey;
1634
+ }): Promise<string> {
1635
+ return instructions.cancelRoundInvestment(
1636
+ this.program,
1637
+ args,
1638
+ this.walletPublicKey
1639
+ );
1640
+ }
1641
+
1642
+ /**
1643
+ * Submit round milestone for investor review
1644
+ *
1645
+ * Multi-Round Fundraising: Transitions milestone to UnderReview,
1646
+ * sets voting deadline.
1647
+ */
1648
+ async submitRoundMilestone(args: {
1649
+ projectId: BN;
1650
+ roundNumber: number;
1651
+ milestoneIndex: number;
1652
+ }): Promise<string> {
1653
+ return instructions.submitRoundMilestone(
1654
+ this.program,
1655
+ args,
1656
+ this.walletPublicKey
1657
+ );
1658
+ }
1659
+
1660
+ /**
1661
+ * Vote on a round milestone (unified voting - ANY investor can vote)
1662
+ *
1663
+ * Multi-Round Fundraising: R1, R2, R3... investors can all vote
1664
+ * on any round's milestones. Investment can be from any round.
1665
+ */
1666
+ async voteOnRoundMilestone(args: {
1667
+ projectId: BN;
1668
+ roundNumber: number;
1669
+ milestoneIndex: number;
1670
+ nftMint: PublicKey | string;
1671
+ choice: { good: object } | { bad: object };
1672
+ investmentRoundNumber: number;
1673
+ }): Promise<string> {
1674
+ return instructions.voteOnRoundMilestone(
1675
+ this.program,
1676
+ args,
1677
+ this.walletPublicKey
1678
+ );
1679
+ }
1680
+
1681
+ /**
1682
+ * Finalize voting on a round milestone
1683
+ *
1684
+ * Multi-Round Fundraising: Processes vote results, transitions
1685
+ * milestone to Passed or Failed state.
1686
+ */
1687
+ async finalizeRoundVoting(args: {
1688
+ projectId: BN;
1689
+ roundNumber: number;
1690
+ milestoneIndex: number;
1691
+ }): Promise<string> {
1692
+ return instructions.finalizeRoundVoting(
1693
+ this.program,
1694
+ args
1695
+ );
1696
+ }
1697
+
1698
+ /**
1699
+ * Claim milestone funds from a round (founder)
1700
+ *
1701
+ * Multi-Round Fundraising: Transfers USDC from round escrow
1702
+ * to founder's account.
1703
+ *
1704
+ * @param nextMilestoneDeadline - Deadline for next milestone (required for non-final)
1705
+ * Set to BN(0) for final milestone claims
1706
+ */
1707
+ async claimRoundMilestoneFunds(args: {
1708
+ projectId: BN;
1709
+ roundNumber: number;
1710
+ milestoneIndex: number;
1711
+ founderUsdcAccount: PublicKey;
1712
+ nextMilestoneDeadline: BN;
1713
+ }): Promise<string> {
1714
+ return instructions.claimRoundMilestoneFunds(
1715
+ this.program,
1716
+ args,
1717
+ this.walletPublicKey
1718
+ );
1719
+ }
1720
+
1721
+ /**
1722
+ * Claim instant tokens for a round milestone (investor)
1723
+ *
1724
+ * Multi-Round Fundraising: R2+ investors claim instant portion
1725
+ * when milestone passes. Tokens come from FutureRoundVault.
1726
+ */
1727
+ async claimRoundInstantTokens(args: {
1728
+ projectId: BN;
1729
+ roundNumber: number;
1730
+ milestoneIndex: number;
1731
+ nftMint: PublicKey;
1732
+ investorTokenAccount: PublicKey;
1733
+ }): Promise<string> {
1734
+ return instructions.claimRoundInstantTokens(
1735
+ this.program,
1736
+ args,
1737
+ this.walletPublicKey
1738
+ );
1739
+ }
1740
+
1741
+ /**
1742
+ * Claim vested tokens for a round milestone (investor)
1743
+ *
1744
+ * Multi-Round Fundraising: R2+ investors claim vested portion
1745
+ * after cliff period. Tokens come from FutureRoundVault.
1746
+ */
1747
+ async claimRoundVestedTokens(args: {
1748
+ projectId: BN;
1749
+ roundNumber: number;
1750
+ milestoneIndex: number;
1751
+ nftMint: PublicKey;
1752
+ investorTokenAccount: PublicKey;
1753
+ }): Promise<string> {
1754
+ return instructions.claimRoundVestedTokens(
1755
+ this.program,
1756
+ args,
1757
+ this.walletPublicKey
1758
+ );
1759
+ }
1760
+
1761
+ /**
1762
+ * Claim early tokens for a round investment (5% after 24h)
1763
+ *
1764
+ * Multi-Round Fundraising: R2+ investors claim 5% of token allocation
1765
+ * 24 hours after investing. Tokens come from FutureRoundVault.
1766
+ */
1767
+ async claimRoundEarlyTokens(args: {
1768
+ projectId: BN;
1769
+ roundNumber: number;
1770
+ nftMint: PublicKey;
1771
+ investorTokenAccount: PublicKey;
1772
+ }): Promise<string> {
1773
+ return instructions.claimRoundEarlyTokens(
1774
+ this.program,
1775
+ args,
1776
+ this.walletPublicKey
1777
+ );
1778
+ }
1779
+
1780
+ // ===========================================================================
1781
+ // Multi-Round Fundraising Helpers
1782
+ // ===========================================================================
1783
+
1784
+ /**
1785
+ * Check if the next funding round can be opened
1786
+ *
1787
+ * Returns eligibility status with reason and next round number.
1788
+ */
1789
+ async canOpenNextRound(projectId: BN): Promise<{
1790
+ canOpen: boolean;
1791
+ reason?: string;
1792
+ nextRoundNumber?: number;
1793
+ }> {
1794
+ return accounts.canOpenNextRound(this.program, projectId);
1795
+ }
1796
+
1797
+ /**
1798
+ * Get remaining future round allocation in BPS
1799
+ */
1800
+ getRemainingFutureRoundAllocation(tokenomics: {
1801
+ futureRoundAllocationBps: number;
1802
+ usedFutureRoundBps?: number;
1803
+ }): number {
1804
+ return accounts.getRemainingFutureRoundAllocation(tokenomics);
1805
+ }
1806
+
1807
+ /**
1808
+ * Check if a specific round is fully funded
1809
+ */
1810
+ async isRoundFullyFunded(projectId: BN, roundNumber: number): Promise<boolean> {
1811
+ return accounts.isRoundFullyFunded(this.program, projectId, roundNumber);
1812
+ }
1813
+
1814
+ /**
1815
+ * Get round details with computed state
1816
+ *
1817
+ * Returns funding round data with additional computed fields.
1818
+ */
1819
+ async getRoundDetails(projectId: BN, roundNumber: number) {
1820
+ const fundingRound = await this.fetchFundingRound(projectId, roundNumber);
1821
+ if (!fundingRound) {
1822
+ return null;
1823
+ }
1824
+
1825
+ const milestones = [];
1826
+ for (let i = 0; i < fundingRound.milestoneCount; i++) {
1827
+ const milestone = await this.fetchRoundMilestone(projectId, roundNumber, i);
1828
+ if (milestone) {
1829
+ milestones.push(milestone);
1830
+ }
1831
+ }
1832
+
1833
+ const totalRaised = fundingRound.totalRaised.toNumber();
1834
+ const fundingGoal = fundingRound.fundingGoal.toNumber();
1835
+ const fundingProgress = fundingGoal > 0 ? (totalRaised / fundingGoal) * 100 : 0;
1836
+
1837
+ return {
1838
+ ...fundingRound,
1839
+ milestones,
1840
+ fundingProgress,
1841
+ isFullyFunded: totalRaised >= fundingGoal,
1842
+ };
1843
+ }
715
1844
  }