@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.
- package/README.md +416 -0
- package/dist/accounts/index.cjs +258 -0
- package/dist/accounts/index.cjs.map +1 -0
- package/dist/accounts/index.d.cts +115 -0
- package/dist/accounts/index.d.ts +115 -0
- package/dist/accounts/index.js +245 -0
- package/dist/accounts/index.js.map +1 -0
- package/dist/constants/index.cjs +174 -0
- package/dist/constants/index.cjs.map +1 -0
- package/dist/constants/index.d.cts +143 -0
- package/dist/constants/index.d.ts +143 -0
- package/dist/constants/index.js +158 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/errors/index.cjs +177 -0
- package/dist/errors/index.cjs.map +1 -0
- package/dist/errors/index.d.cts +83 -0
- package/dist/errors/index.d.ts +83 -0
- package/dist/errors/index.js +170 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.cjs +2063 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +680 -0
- package/dist/index.d.ts +680 -0
- package/dist/index.js +1926 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/index.cjs +852 -0
- package/dist/instructions/index.cjs.map +1 -0
- package/dist/instructions/index.d.cts +452 -0
- package/dist/instructions/index.d.ts +452 -0
- package/dist/instructions/index.js +809 -0
- package/dist/instructions/index.js.map +1 -0
- package/dist/pdas/index.cjs +241 -0
- package/dist/pdas/index.cjs.map +1 -0
- package/dist/pdas/index.d.cts +171 -0
- package/dist/pdas/index.d.ts +171 -0
- package/dist/pdas/index.js +217 -0
- package/dist/pdas/index.js.map +1 -0
- package/dist/types/index.cjs +44 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +229 -0
- package/dist/types/index.d.ts +229 -0
- package/dist/types/index.js +39 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +130 -0
- package/src/accounts/index.ts +329 -0
- package/src/client.ts +715 -0
- package/src/constants/index.ts +205 -0
- package/src/errors/index.ts +222 -0
- package/src/events/index.ts +256 -0
- package/src/index.ts +253 -0
- package/src/instructions/index.ts +1504 -0
- package/src/pdas/index.ts +404 -0
- package/src/types/index.ts +267 -0
- 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
|
+
}
|