@zcomb/programs-sdk 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/futarchy/client.d.ts +132 -5
  2. package/dist/futarchy/client.js +80 -21
  3. package/dist/futarchy/instructions.d.ts +109 -1
  4. package/dist/futarchy/instructions.js +17 -3
  5. package/dist/generated/idls/futarchy.json +112 -0
  6. package/dist/generated/types/futarchy.d.ts +112 -0
  7. package/dist/generated/types/futarchy.js +1 -1
  8. package/package.json +1 -2
  9. package/src/amm/client.ts +0 -485
  10. package/src/amm/constants.ts +0 -31
  11. package/src/amm/index.ts +0 -5
  12. package/src/amm/instructions.ts +0 -139
  13. package/src/amm/types.ts +0 -62
  14. package/src/amm/utils.ts +0 -263
  15. package/src/futarchy/client.ts +0 -1032
  16. package/src/futarchy/constants.ts +0 -28
  17. package/src/futarchy/index.ts +0 -5
  18. package/src/futarchy/instructions.ts +0 -235
  19. package/src/futarchy/types.ts +0 -54
  20. package/src/futarchy/utils.ts +0 -108
  21. package/src/generated/idls/amm.json +0 -1252
  22. package/src/generated/idls/futarchy.json +0 -1763
  23. package/src/generated/idls/index.ts +0 -4
  24. package/src/generated/idls/svault.json +0 -2228
  25. package/src/generated/idls/vault.json +0 -1501
  26. package/src/generated/types/amm.ts +0 -1258
  27. package/src/generated/types/futarchy.ts +0 -1769
  28. package/src/generated/types/index.ts +0 -4
  29. package/src/generated/types/svault.ts +0 -2234
  30. package/src/generated/types/vault.ts +0 -1507
  31. package/src/index.ts +0 -163
  32. package/src/svault/client.ts +0 -401
  33. package/src/svault/constants.ts +0 -23
  34. package/src/svault/index.ts +0 -5
  35. package/src/svault/instructions.ts +0 -258
  36. package/src/svault/types.ts +0 -45
  37. package/src/svault/utils.ts +0 -145
  38. package/src/utils.ts +0 -41
  39. package/src/vault/client.ts +0 -333
  40. package/src/vault/constants.ts +0 -23
  41. package/src/vault/index.ts +0 -5
  42. package/src/vault/instructions.ts +0 -170
  43. package/src/vault/types.ts +0 -54
  44. package/src/vault/utils.ts +0 -70
@@ -1,1032 +0,0 @@
1
- /*
2
- * High-level client for the Futarchy program.
3
- * Handles account derivation, instruction building, and transaction composition.
4
- */
5
-
6
- import { Program, AnchorProvider, BN } from "@coral-xyz/anchor";
7
- import {
8
- PublicKey,
9
- ComputeBudgetProgram,
10
- AddressLookupTableProgram,
11
- AddressLookupTableAccount,
12
- SystemProgram,
13
- Transaction,
14
- TransactionInstruction,
15
- TransactionMessage,
16
- VersionedTransaction,
17
- } from "@solana/web3.js";
18
- import {
19
- getAssociatedTokenAddressSync,
20
- TOKEN_PROGRAM_ID,
21
- ASSOCIATED_TOKEN_PROGRAM_ID,
22
- createAssociatedTokenAccountIdempotentInstruction,
23
- } from "@solana/spl-token";
24
- import { PROGRAM_ID, SQUADS_PROGRAM_ID } from "./constants";
25
- import {
26
- Futarchy,
27
- DAOAccount,
28
- ModeratorAccount,
29
- ProposalAccount,
30
- ProposalParams,
31
- PoolType,
32
- } from "./types";
33
- import {
34
- deriveDAOPDA,
35
- deriveModeratorPDA,
36
- deriveProposalPDA,
37
- deriveMintCreateKeyPDA,
38
- fetchDAOAccount,
39
- fetchModeratorAccount,
40
- fetchProposalAccount,
41
- parseProposalState,
42
- isProposalExpired,
43
- getTimeRemaining,
44
- } from "./utils";
45
- import {
46
- initializeModerator,
47
- initializeProposal,
48
- addOption,
49
- launchProposal,
50
- finalizeProposal,
51
- redeemLiquidity,
52
- addHistoricalProposal,
53
- initializeParentDAO,
54
- initializeChildDAO,
55
- upgradeDAO,
56
- } from "./instructions";
57
- import { TxOptions } from "../utils";
58
-
59
- import { VaultClient, deriveVaultPDA, deriveConditionalMint, VaultType } from "../vault";
60
- import { AMMClient, derivePoolPDA, deriveReservePDA, deriveFeeVaultPDA, FEE_AUTHORITY } from "../amm";
61
-
62
- import { FutarchyIDL } from "../generated/idls";
63
- import * as multisig from "@sqds/multisig";
64
-
65
- const DEFAULT_COMPUTE_UNITS = 500_000;
66
-
67
- export class FutarchyClient {
68
- public program: Program<Futarchy>;
69
- public programId: PublicKey;
70
- public vault: VaultClient;
71
- public amm: AMMClient;
72
- private defaultComputeUnits: number;
73
-
74
- constructor(provider: AnchorProvider, programId?: PublicKey, computeUnits?: number) {
75
- this.programId = programId ?? PROGRAM_ID;
76
- this.program = new Program(FutarchyIDL as Futarchy, provider);
77
- this.vault = new VaultClient(provider);
78
- this.amm = new AMMClient(provider);
79
- this.defaultComputeUnits = computeUnits ?? DEFAULT_COMPUTE_UNITS;
80
- }
81
-
82
- /* PDA Helpers */
83
-
84
- deriveDAOPDA(name: string): [PublicKey, number] {
85
- return deriveDAOPDA(name, this.programId);
86
- }
87
-
88
- deriveModeratorPDA(name: string): [PublicKey, number] {
89
- return deriveModeratorPDA(name, this.programId);
90
- }
91
-
92
- deriveProposalPDA(moderator: PublicKey, proposalId: number): [PublicKey, number] {
93
- return deriveProposalPDA(moderator, proposalId, this.programId);
94
- }
95
-
96
- /* Fetchers */
97
-
98
- async fetchDAO(daoPda: PublicKey): Promise<DAOAccount> {
99
- return fetchDAOAccount(this.program, daoPda);
100
- }
101
-
102
- async fetchModerator(moderatorPda: PublicKey): Promise<ModeratorAccount> {
103
- return fetchModeratorAccount(this.program, moderatorPda);
104
- }
105
-
106
- async fetchProposal(proposalPda: PublicKey): Promise<ProposalAccount> {
107
- return fetchProposalAccount(this.program, proposalPda);
108
- }
109
-
110
- /* Proposal Helpers */
111
-
112
- isProposalExpired(proposal: ProposalAccount): boolean {
113
- return isProposalExpired(proposal);
114
- }
115
-
116
- getTimeRemaining(proposal: ProposalAccount): number {
117
- return getTimeRemaining(proposal);
118
- }
119
-
120
- private getComputeUnits(options?: TxOptions): number {
121
- return options?.computeUnits ?? this.defaultComputeUnits;
122
- }
123
-
124
- private maybeAddComputeBudget(options?: TxOptions): TransactionInstruction[] {
125
- if (options?.includeCuBudget === false) {
126
- return [];
127
- }
128
- return [ComputeBudgetProgram.setComputeUnitLimit({ units: this.getComputeUnits(options) })];
129
- }
130
-
131
- /* Instruction Builders */
132
-
133
- async initializeModerator(
134
- admin: PublicKey,
135
- baseMint: PublicKey,
136
- quoteMint: PublicKey,
137
- name: string,
138
- options?: TxOptions
139
- ) {
140
- const [moderatorPda] = this.deriveModeratorPDA(name);
141
-
142
- const builder = initializeModerator(
143
- this.program,
144
- admin,
145
- baseMint,
146
- quoteMint,
147
- moderatorPda,
148
- name
149
- ).preInstructions(this.maybeAddComputeBudget(options));
150
-
151
- return { builder, moderatorPda, name };
152
- }
153
-
154
- async addHistoricalProposal(
155
- admin: PublicKey,
156
- moderatorPda: PublicKey,
157
- numOptions: number,
158
- winningIdx: number,
159
- length: number,
160
- createdAt: BN | number,
161
- options?: TxOptions
162
- ) {
163
- const moderator = await this.fetchModerator(moderatorPda);
164
- const proposalId = moderator.proposalIdCounter;
165
- const [proposalPda] = this.deriveProposalPDA(moderatorPda, proposalId);
166
-
167
- const builder = addHistoricalProposal(
168
- this.program,
169
- admin,
170
- moderatorPda,
171
- proposalPda,
172
- numOptions,
173
- winningIdx,
174
- length,
175
- createdAt
176
- ).preInstructions(this.maybeAddComputeBudget(options));
177
-
178
- return { builder, proposalPda, proposalId };
179
- }
180
-
181
- async initializeProposal(
182
- creator: PublicKey,
183
- moderatorPda: PublicKey,
184
- proposalParams: ProposalParams,
185
- metadata?: string,
186
- options?: TxOptions
187
- ) {
188
- const moderator = await this.fetchModerator(moderatorPda);
189
- const proposalId = moderator.proposalIdCounter;
190
- const [proposalPda] = this.deriveProposalPDA(moderatorPda, proposalId);
191
-
192
- // Derive vault PDA (proposal is the owner, nonce=proposalId)
193
- const [vaultPda] = deriveVaultPDA(proposalPda, proposalId, this.vault.programId);
194
-
195
- // Derive conditional mints for initial 2 options
196
- const [condBaseMint0] = deriveConditionalMint(vaultPda, VaultType.Base, 0, this.vault.programId);
197
- const [condBaseMint1] = deriveConditionalMint(vaultPda, VaultType.Base, 1, this.vault.programId);
198
- const [condQuoteMint0] = deriveConditionalMint(vaultPda, VaultType.Quote, 0, this.vault.programId);
199
- const [condQuoteMint1] = deriveConditionalMint(vaultPda, VaultType.Quote, 1, this.vault.programId);
200
-
201
- // Derive pool PDAs for initial 2 options (proposal is admin, mintA=condQuote, mintB=condBase)
202
- const [pool0] = derivePoolPDA(proposalPda, condQuoteMint0, condBaseMint0, this.amm.programId);
203
- const [pool1] = derivePoolPDA(proposalPda, condQuoteMint1, condBaseMint1, this.amm.programId);
204
-
205
- // Derive reserves and fee vaults
206
- const [reserveA0] = deriveReservePDA(pool0, condQuoteMint0, this.amm.programId);
207
- const [reserveB0] = deriveReservePDA(pool0, condBaseMint0, this.amm.programId);
208
- const [feeVault0] = deriveFeeVaultPDA(pool0, this.amm.programId);
209
- const [reserveA1] = deriveReservePDA(pool1, condQuoteMint1, this.amm.programId);
210
- const [reserveB1] = deriveReservePDA(pool1, condBaseMint1, this.amm.programId);
211
- const [feeVault1] = deriveFeeVaultPDA(pool1, this.amm.programId);
212
-
213
- // Vault token accounts
214
- const baseTokenAcc = getAssociatedTokenAddressSync(moderator.baseMint, vaultPda, true);
215
- const quoteTokenAcc = getAssociatedTokenAddressSync(moderator.quoteMint, vaultPda, true);
216
-
217
- // Build remaining accounts in expected order (see initialize_proposal.rs)
218
- const remainingAccounts = [
219
- { pubkey: moderator.baseMint, isSigner: false, isWritable: false }, // 0: base_mint
220
- { pubkey: moderator.quoteMint, isSigner: false, isWritable: false }, // 1: quote_mint
221
- { pubkey: vaultPda, isSigner: false, isWritable: true }, // 2: vault
222
- { pubkey: baseTokenAcc, isSigner: false, isWritable: true }, // 3: base_token_acc
223
- { pubkey: quoteTokenAcc, isSigner: false, isWritable: true }, // 4: quote_token_acc
224
- { pubkey: condBaseMint0, isSigner: false, isWritable: true }, // 5: cond_base_mint_0
225
- { pubkey: condBaseMint1, isSigner: false, isWritable: true }, // 6: cond_base_mint_1
226
- { pubkey: condQuoteMint0, isSigner: false, isWritable: true }, // 7: cond_quote_mint_0
227
- { pubkey: condQuoteMint1, isSigner: false, isWritable: true }, // 8: cond_quote_mint_1
228
- { pubkey: pool0, isSigner: false, isWritable: true }, // 9: pool_0
229
- { pubkey: reserveA0, isSigner: false, isWritable: true }, // 10: reserve_a_0
230
- { pubkey: reserveB0, isSigner: false, isWritable: true }, // 11: reserve_b_0
231
- { pubkey: FEE_AUTHORITY, isSigner: false, isWritable: false }, // 12: fee_authority
232
- { pubkey: feeVault0, isSigner: false, isWritable: true }, // 13: fee_vault_0
233
- { pubkey: pool1, isSigner: false, isWritable: true }, // 14: pool_1
234
- { pubkey: reserveA1, isSigner: false, isWritable: true }, // 15: reserve_a_1
235
- { pubkey: reserveB1, isSigner: false, isWritable: true }, // 16: reserve_b_1
236
- { pubkey: feeVault1, isSigner: false, isWritable: true }, // 17: fee_vault_1
237
- ];
238
-
239
- const builder = initializeProposal(
240
- this.program,
241
- creator,
242
- moderatorPda,
243
- proposalPda,
244
- proposalParams,
245
- metadata ?? null,
246
- remainingAccounts
247
- ).preInstructions(this.maybeAddComputeBudget(options));
248
-
249
- return {
250
- builder,
251
- proposalPda,
252
- proposalId,
253
- vaultPda,
254
- pools: [pool0, pool1],
255
- condBaseMints: [condBaseMint0, condBaseMint1],
256
- condQuoteMints: [condQuoteMint0, condQuoteMint1],
257
- };
258
- }
259
-
260
- async addOption(creator: PublicKey, proposalPda: PublicKey, options?: TxOptions) {
261
- const proposal = await this.fetchProposal(proposalPda);
262
- const optionIndex = proposal.numOptions;
263
-
264
- // Derive new conditional mints
265
- const [condBaseMint] = deriveConditionalMint(proposal.vault, VaultType.Base, optionIndex, this.vault.programId);
266
- const [condQuoteMint] = deriveConditionalMint(proposal.vault, VaultType.Quote, optionIndex, this.vault.programId);
267
-
268
- // Derive pool PDA
269
- const [pool] = derivePoolPDA(proposalPda, condQuoteMint, condBaseMint, this.amm.programId);
270
- const [reserveA] = deriveReservePDA(pool, condQuoteMint, this.amm.programId);
271
- const [reserveB] = deriveReservePDA(pool, condBaseMint, this.amm.programId);
272
- const [feeVault] = deriveFeeVaultPDA(pool, this.amm.programId);
273
-
274
- // Build remaining accounts (see add_option.rs)
275
- const remainingAccounts = [
276
- { pubkey: proposal.vault, isSigner: false, isWritable: true }, // 0: vault
277
- { pubkey: condBaseMint, isSigner: false, isWritable: true }, // 1: cond_base_mint
278
- { pubkey: condQuoteMint, isSigner: false, isWritable: true }, // 2: cond_quote_mint
279
- { pubkey: pool, isSigner: false, isWritable: true }, // 3: pool
280
- { pubkey: reserveA, isSigner: false, isWritable: true }, // 4: reserve_a
281
- { pubkey: reserveB, isSigner: false, isWritable: true }, // 5: reserve_b
282
- { pubkey: FEE_AUTHORITY, isSigner: false, isWritable: false }, // 6: fee_authority
283
- { pubkey: feeVault, isSigner: false, isWritable: true }, // 7: fee_vault
284
- ];
285
-
286
- const builder = addOption(this.program, creator, proposalPda, remainingAccounts)
287
- .preInstructions(this.maybeAddComputeBudget(options));
288
-
289
- return { builder, optionIndex, pool, condBaseMint, condQuoteMint };
290
- }
291
-
292
- async launchProposal(
293
- creator: PublicKey,
294
- proposalPda: PublicKey,
295
- baseAmount: BN | number,
296
- quoteAmount: BN | number,
297
- options?: TxOptions
298
- ) {
299
- const proposal = await this.fetchProposal(proposalPda);
300
- const vault = await this.vault.fetchVault(proposal.vault);
301
- const numOptions = proposal.numOptions;
302
-
303
- // Slice arrays to numOptions (fixed-size arrays from Rust include empty slots)
304
- const condBaseMints = vault.condBaseMints.slice(0, numOptions);
305
- const condQuoteMints = vault.condQuoteMints.slice(0, numOptions);
306
-
307
- // Pre-create conditional ATAs for 3+ options to avoid exceeding the
308
- // 64 instruction trace limit. Each ATA creation via vault deposit adds
309
- // 5 inner instructions; with 4 options that's 40 extra instructions.
310
- const shouldEnsureATAs = options?.ensureATAs ?? (numOptions >= 3);
311
- if (shouldEnsureATAs) {
312
- await this._createConditionalATAs(creator, condBaseMints, condQuoteMints);
313
- }
314
- const pools = proposal.pools.slice(0, numOptions);
315
-
316
- // Derive all user conditional token ATAs
317
- const userCondBaseATAs = condBaseMints.map((m) => getAssociatedTokenAddressSync(m, creator));
318
- const userCondQuoteATAs = condQuoteMints.map((m) => getAssociatedTokenAddressSync(m, creator));
319
-
320
- // Derive reserve accounts for each pool
321
- const reservesA: PublicKey[] = [];
322
- const reservesB: PublicKey[] = [];
323
- for (let i = 0; i < numOptions; i++) {
324
- const [resA] = deriveReservePDA(pools[i], condQuoteMints[i], this.amm.programId);
325
- const [resB] = deriveReservePDA(pools[i], condBaseMints[i], this.amm.programId);
326
- reservesA.push(resA);
327
- reservesB.push(resB);
328
- }
329
-
330
- // Build remaining accounts (see launch_proposal.rs)
331
- // Layout: 6 fixed + 7*N variable
332
- const remainingAccounts: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] = [
333
- { pubkey: vault.baseMint.address, isSigner: false, isWritable: false }, // 0: base_mint
334
- { pubkey: vault.quoteMint.address, isSigner: false, isWritable: false }, // 1: quote_mint
335
- { pubkey: getAssociatedTokenAddressSync(vault.baseMint.address, proposal.vault, true), isSigner: false, isWritable: true }, // 2: vault_base_ata
336
- { pubkey: getAssociatedTokenAddressSync(vault.quoteMint.address, proposal.vault, true), isSigner: false, isWritable: true }, // 3: vault_quote_ata
337
- { pubkey: getAssociatedTokenAddressSync(vault.baseMint.address, creator), isSigner: false, isWritable: true }, // 4: user_base_ata
338
- { pubkey: getAssociatedTokenAddressSync(vault.quoteMint.address, creator), isSigner: false, isWritable: true }, // 5: user_quote_ata
339
- ];
340
-
341
- // 6..6+N: cond_base_mints
342
- for (const mint of condBaseMints) {
343
- remainingAccounts.push({ pubkey: mint, isSigner: false, isWritable: true });
344
- }
345
- // 6+N..6+2N: cond_quote_mints
346
- for (const mint of condQuoteMints) {
347
- remainingAccounts.push({ pubkey: mint, isSigner: false, isWritable: true });
348
- }
349
- // 6+2N..6+3N: user_cond_base_atas
350
- for (const ata of userCondBaseATAs) {
351
- remainingAccounts.push({ pubkey: ata, isSigner: false, isWritable: true });
352
- }
353
- // 6+3N..6+4N: user_cond_quote_atas
354
- for (const ata of userCondQuoteATAs) {
355
- remainingAccounts.push({ pubkey: ata, isSigner: false, isWritable: true });
356
- }
357
- // 6+4N..6+5N: pools
358
- for (const pool of pools) {
359
- remainingAccounts.push({ pubkey: pool, isSigner: false, isWritable: true });
360
- }
361
- // 6+5N..6+6N: reserves_a
362
- for (const res of reservesA) {
363
- remainingAccounts.push({ pubkey: res, isSigner: false, isWritable: true });
364
- }
365
- // 6+6N..6+7N: reserves_b
366
- for (const res of reservesB) {
367
- remainingAccounts.push({ pubkey: res, isSigner: false, isWritable: true });
368
- }
369
-
370
- const builder = launchProposal(
371
- this.program,
372
- creator,
373
- proposalPda,
374
- proposal.vault,
375
- baseAmount,
376
- quoteAmount,
377
- remainingAccounts
378
- ).preInstructions(this.maybeAddComputeBudget(options));
379
-
380
- return { builder };
381
- }
382
-
383
- /**
384
- * Pre-creates all conditional token ATAs for a user before launching a proposal.
385
- *
386
- * This is REQUIRED for proposals with 3+ options to avoid exceeding Solana's
387
- * max instruction trace length limit (64 instructions). The vault's deposit CPI
388
- * creates ATAs on-the-fly, each requiring 5 inner instructions. For 4 options:
389
- * 8 ATAs × 5 = 40 extra instructions, pushing the total over 64.
390
- *
391
- * Pre-creating ATAs eliminates this overhead, reducing the trace to ~32 instructions.
392
- *
393
- * @param creator - The user who will receive conditional tokens
394
- * @param proposalPda - The proposal PDA (must be initialized but not launched)
395
- * @returns Transaction signature
396
- */
397
- async ensureConditionalATAs(
398
- creator: PublicKey,
399
- proposalPda: PublicKey,
400
- ): Promise<string> {
401
- const proposal = await this.fetchProposal(proposalPda);
402
- const vault = await this.vault.fetchVault(proposal.vault);
403
- const condBaseMints = vault.condBaseMints.slice(0, proposal.numOptions);
404
- const condQuoteMints = vault.condQuoteMints.slice(0, proposal.numOptions);
405
- return this._createConditionalATAs(creator, condBaseMints, condQuoteMints);
406
- }
407
-
408
- /**
409
- * Internal helper to create conditional ATAs given mint arrays.
410
- * Used by both ensureConditionalATAs and launchProposal to avoid redundant fetches.
411
- */
412
- private async _createConditionalATAs(
413
- creator: PublicKey,
414
- condBaseMints: PublicKey[],
415
- condQuoteMints: PublicKey[],
416
- ): Promise<string> {
417
- const provider = this.program.provider as AnchorProvider;
418
-
419
- // Build ATA creation instructions (idempotent - won't fail if exists)
420
- const instructions: TransactionInstruction[] = [];
421
- for (let i = 0; i < condBaseMints.length; i++) {
422
- const userCondBaseAta = getAssociatedTokenAddressSync(condBaseMints[i], creator);
423
- const userCondQuoteAta = getAssociatedTokenAddressSync(condQuoteMints[i], creator);
424
-
425
- instructions.push(
426
- createAssociatedTokenAccountIdempotentInstruction(
427
- creator,
428
- userCondBaseAta,
429
- creator,
430
- condBaseMints[i]
431
- ),
432
- createAssociatedTokenAccountIdempotentInstruction(
433
- creator,
434
- userCondQuoteAta,
435
- creator,
436
- condQuoteMints[i]
437
- )
438
- );
439
- }
440
-
441
- // Send transaction
442
- const tx = new Transaction().add(...instructions);
443
- return provider.sendAndConfirm(tx);
444
- }
445
-
446
- async finalizeProposal(signer: PublicKey, proposalPda: PublicKey, options?: TxOptions) {
447
- const proposal = await this.fetchProposal(proposalPda);
448
- const vault = await this.vault.fetchVault(proposal.vault);
449
- const numOptions = proposal.numOptions;
450
-
451
- // Build remaining accounts (3 per pool: pool, reserve_a, reserve_b)
452
- const remainingAccounts: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] = [];
453
-
454
- for (let i = 0; i < numOptions; i++) {
455
- const pool = proposal.pools[i];
456
- const [reserveA] = deriveReservePDA(pool, vault.condQuoteMints[i], this.amm.programId);
457
- const [reserveB] = deriveReservePDA(pool, vault.condBaseMints[i], this.amm.programId);
458
-
459
- remainingAccounts.push({ pubkey: pool, isSigner: false, isWritable: true });
460
- remainingAccounts.push({ pubkey: reserveA, isSigner: false, isWritable: false });
461
- remainingAccounts.push({ pubkey: reserveB, isSigner: false, isWritable: false });
462
- }
463
-
464
- const builder = finalizeProposal(
465
- this.program,
466
- signer,
467
- proposalPda,
468
- proposal.vault,
469
- remainingAccounts
470
- ).preInstructions(this.maybeAddComputeBudget(options));
471
-
472
- return { builder };
473
- }
474
-
475
- async redeemLiquidity(creator: PublicKey, proposalPda: PublicKey, options?: TxOptions) {
476
- const proposal = await this.fetchProposal(proposalPda);
477
- const vault = await this.vault.fetchVault(proposal.vault);
478
- const numOptions = proposal.numOptions;
479
-
480
- const { winningIdx } = parseProposalState(proposal.state);
481
- if (winningIdx === null) {
482
- throw new Error("Proposal not finalized");
483
- }
484
- const winningPool = proposal.pools[winningIdx];
485
-
486
- // Derive winning pool reserves
487
- const [reserveA] = deriveReservePDA(winningPool, vault.condQuoteMints[winningIdx], this.amm.programId);
488
- const [reserveB] = deriveReservePDA(winningPool, vault.condBaseMints[winningIdx], this.amm.programId);
489
-
490
- // User's winning conditional token ATAs
491
- const creatorCondQuoteAta = getAssociatedTokenAddressSync(vault.condQuoteMints[winningIdx], creator);
492
- const creatorCondBaseAta = getAssociatedTokenAddressSync(vault.condBaseMints[winningIdx], creator);
493
-
494
- // Build remaining accounts (see redeem_liquidity.rs)
495
- const remainingAccounts: { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] = [
496
- // remove_liquidity accounts (0-3)
497
- { pubkey: reserveA, isSigner: false, isWritable: true },
498
- { pubkey: reserveB, isSigner: false, isWritable: true },
499
- { pubkey: creatorCondQuoteAta, isSigner: false, isWritable: true },
500
- { pubkey: creatorCondBaseAta, isSigner: false, isWritable: true },
501
-
502
- // redeem_winnings base fixed accounts (4-6)
503
- { pubkey: vault.baseMint.address, isSigner: false, isWritable: false },
504
- { pubkey: getAssociatedTokenAddressSync(vault.baseMint.address, proposal.vault, true), isSigner: false, isWritable: true },
505
- { pubkey: getAssociatedTokenAddressSync(vault.baseMint.address, creator), isSigner: false, isWritable: true },
506
- ];
507
-
508
- // redeem_winnings base remaining (7..7+2N): [cond_base_mint_i, user_cond_base_ata_i]
509
- for (let i = 0; i < numOptions; i++) {
510
- remainingAccounts.push({ pubkey: vault.condBaseMints[i], isSigner: false, isWritable: true });
511
- remainingAccounts.push({ pubkey: getAssociatedTokenAddressSync(vault.condBaseMints[i], creator), isSigner: false, isWritable: true });
512
- }
513
-
514
- // redeem_winnings quote fixed accounts
515
- remainingAccounts.push({ pubkey: vault.quoteMint.address, isSigner: false, isWritable: false });
516
- remainingAccounts.push({ pubkey: getAssociatedTokenAddressSync(vault.quoteMint.address, proposal.vault, true), isSigner: false, isWritable: true });
517
- remainingAccounts.push({ pubkey: getAssociatedTokenAddressSync(vault.quoteMint.address, creator), isSigner: false, isWritable: true });
518
-
519
- // redeem_winnings quote remaining: [cond_quote_mint_i, user_cond_quote_ata_i]
520
- for (let i = 0; i < numOptions; i++) {
521
- remainingAccounts.push({ pubkey: vault.condQuoteMints[i], isSigner: false, isWritable: true });
522
- remainingAccounts.push({ pubkey: getAssociatedTokenAddressSync(vault.condQuoteMints[i], creator), isSigner: false, isWritable: true });
523
- }
524
-
525
- const builder = redeemLiquidity(
526
- this.program,
527
- creator,
528
- proposalPda,
529
- proposal.vault,
530
- winningPool,
531
- remainingAccounts
532
- ).preInstructions(this.maybeAddComputeBudget(options));
533
-
534
- return { builder, numOptions };
535
- }
536
-
537
- /**
538
- * Creates an Address Lookup Table for redemption operations.
539
- * Required for proposals with 3+ options to avoid exceeding transaction size limits.
540
- *
541
- * @param creator - The user redeeming (creator of proposal or liquidity provider)
542
- * @param proposalPda - The proposal PDA
543
- * @returns ALT address
544
- */
545
- async createRedemptionALT(
546
- creator: PublicKey,
547
- proposalPda: PublicKey,
548
- ): Promise<{ altAddress: PublicKey }> {
549
- const provider = this.program.provider as AnchorProvider;
550
- const proposal = await this.fetchProposal(proposalPda);
551
- const vault = await this.vault.fetchVault(proposal.vault);
552
- const numOptions = proposal.numOptions;
553
-
554
- const { winningIdx } = parseProposalState(proposal.state);
555
- if (winningIdx === null) {
556
- throw new Error("Proposal not finalized");
557
- }
558
-
559
- const addresses: PublicKey[] = [
560
- // Programs
561
- this.programId,
562
- this.vault.programId,
563
- this.amm.programId,
564
- SystemProgram.programId,
565
- TOKEN_PROGRAM_ID,
566
- ASSOCIATED_TOKEN_PROGRAM_ID,
567
- // Core accounts
568
- proposalPda,
569
- proposal.vault,
570
- proposal.moderator,
571
- vault.baseMint.address,
572
- vault.quoteMint.address,
573
- // Winning pool and reserves
574
- proposal.pools[winningIdx],
575
- // Vault token accounts
576
- getAssociatedTokenAddressSync(vault.baseMint.address, proposal.vault, true),
577
- getAssociatedTokenAddressSync(vault.quoteMint.address, proposal.vault, true),
578
- // Creator's base/quote ATAs
579
- getAssociatedTokenAddressSync(vault.baseMint.address, creator),
580
- getAssociatedTokenAddressSync(vault.quoteMint.address, creator),
581
- ];
582
-
583
- // Winning pool reserves
584
- const [reserveA] = deriveReservePDA(proposal.pools[winningIdx], vault.condQuoteMints[winningIdx], this.amm.programId);
585
- const [reserveB] = deriveReservePDA(proposal.pools[winningIdx], vault.condBaseMints[winningIdx], this.amm.programId);
586
- addresses.push(reserveA, reserveB);
587
-
588
- // Per-option accounts (conditional mints and user ATAs)
589
- for (let i = 0; i < numOptions; i++) {
590
- addresses.push(
591
- vault.condBaseMints[i],
592
- vault.condQuoteMints[i],
593
- getAssociatedTokenAddressSync(vault.condBaseMints[i], creator),
594
- getAssociatedTokenAddressSync(vault.condQuoteMints[i], creator),
595
- );
596
- }
597
-
598
- // Get recent slot for ALT creation
599
- const slot = await provider.connection.getSlot("finalized");
600
-
601
- const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({
602
- authority: creator,
603
- payer: creator,
604
- recentSlot: slot,
605
- });
606
-
607
- // Create ALT
608
- const createTx = new Transaction().add(createIx);
609
- createTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash;
610
- createTx.feePayer = creator;
611
- const signedTx = await provider.wallet.signTransaction(createTx);
612
- const sig = await provider.connection.sendRawTransaction(signedTx.serialize(), {
613
- skipPreflight: true,
614
- });
615
- await provider.connection.confirmTransaction(sig, "confirmed");
616
-
617
- // Extend ALT with addresses
618
- // Use skipPreflight to avoid race condition where simulation sees stale state
619
- // before previous extend has propagated (same pattern as CREATE above)
620
- const CHUNK_SIZE = 20;
621
- for (let i = 0; i < addresses.length; i += CHUNK_SIZE) {
622
- const chunk = addresses.slice(i, i + CHUNK_SIZE);
623
- const extendIx = AddressLookupTableProgram.extendLookupTable({
624
- payer: creator,
625
- authority: creator,
626
- lookupTable: altAddress,
627
- addresses: chunk,
628
- });
629
- const extendTx = new Transaction().add(extendIx);
630
- extendTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash;
631
- extendTx.feePayer = creator;
632
- const signedExtendTx = await provider.wallet.signTransaction(extendTx);
633
- const extendSig = await provider.connection.sendRawTransaction(signedExtendTx.serialize(), {
634
- skipPreflight: true,
635
- });
636
- await provider.connection.confirmTransaction(extendSig, "confirmed");
637
- }
638
-
639
- return { altAddress };
640
- }
641
-
642
- /**
643
- * Builds a versioned transaction for redeeming liquidity with ALT.
644
- * Required for proposals with 3+ options to avoid exceeding transaction size limits.
645
- *
646
- * @param creator - The user redeeming
647
- * @param proposalPda - The proposal PDA
648
- * @param altAddress - Optional ALT address (will be created if not provided for 3+ options)
649
- * @returns Unsigned versioned transaction, ALT address, and number of options
650
- */
651
- async redeemLiquidityVersioned(
652
- creator: PublicKey,
653
- proposalPda: PublicKey,
654
- altAddress?: PublicKey,
655
- ): Promise<{ versionedTx: VersionedTransaction; altAddress: PublicKey; numOptions: number }> {
656
- const provider = this.program.provider as AnchorProvider;
657
- const { builder, numOptions } = await this.redeemLiquidity(creator, proposalPda);
658
-
659
- // Create ALT if not provided and needed
660
- let altPubkey = altAddress;
661
- let verifiedALT: AddressLookupTableAccount | null = null;
662
-
663
- if (!altPubkey && numOptions >= 3) {
664
- console.log(` Creating redemption ALT for ${numOptions} options...`);
665
- const result = await this.createRedemptionALT(creator, proposalPda);
666
- altPubkey = result.altAddress;
667
- console.log(` ✓ Redemption ALT created: ${altPubkey.toBase58()}`);
668
-
669
- // Wait for ALT to be fully available with all addresses
670
- // Use longer delays after extending to ensure propagation
671
- console.log(` Waiting for ALT propagation...`);
672
- const expectedAddresses = 18 + (numOptions * 4); // Base accounts + per-option accounts
673
- let attempts = 0;
674
-
675
- // Initial delay after extension
676
- await new Promise(resolve => setTimeout(resolve, 2000));
677
-
678
- while (attempts < 30) {
679
- const altAccount = await provider.connection.getAddressLookupTable(altPubkey, {
680
- commitment: 'confirmed',
681
- });
682
- if (altAccount.value) {
683
- const addressCount = altAccount.value.state.addresses.length;
684
- console.log(` Attempt ${attempts + 1}: ALT has ${addressCount}/${expectedAddresses} addresses`);
685
- if (addressCount >= expectedAddresses) {
686
- verifiedALT = altAccount.value;
687
- console.log(` ✓ ALT verified with ${addressCount} addresses`);
688
- break;
689
- }
690
- }
691
- await new Promise(resolve => setTimeout(resolve, 1000));
692
- attempts++;
693
- }
694
-
695
- if (!verifiedALT) {
696
- throw new Error(`ALT failed to populate with expected ${expectedAddresses} addresses after ${attempts} attempts`);
697
- }
698
- }
699
-
700
- if (!altPubkey) {
701
- throw new Error("ALT address required for multi-option redemption");
702
- }
703
-
704
- // Fetch ALT if we don't have it verified already
705
- if (!verifiedALT) {
706
- verifiedALT = await this.fetchALT(altPubkey);
707
- }
708
-
709
- // Build instruction
710
- const instruction = await builder.instruction();
711
- const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 });
712
-
713
- // Get fresh blockhash
714
- const { blockhash } = await provider.connection.getLatestBlockhash();
715
-
716
- // Build versioned transaction using the verified ALT (no re-fetch)
717
- const versionedTx = this.buildVersionedTxWithALT(
718
- creator,
719
- [computeBudgetIx, instruction],
720
- verifiedALT,
721
- blockhash,
722
- );
723
-
724
- // Return the versioned transaction for the caller to sign and send
725
- // This allows the caller to use their own signing mechanism (e.g., keypair.sign)
726
- return { versionedTx, altAddress: altPubkey, numOptions };
727
- }
728
-
729
- /**
730
- * Helper to send a signed versioned transaction.
731
- */
732
- async sendVersionedTransaction(
733
- signedTx: VersionedTransaction,
734
- ): Promise<string> {
735
- const provider = this.program.provider as AnchorProvider;
736
- const signature = await provider.connection.sendTransaction(signedTx, {
737
- skipPreflight: false,
738
- preflightCommitment: 'confirmed',
739
- });
740
- await provider.connection.confirmTransaction(signature, 'confirmed');
741
- return signature;
742
- }
743
-
744
- /* Address Lookup Table */
745
-
746
- async createProposalALT(
747
- creator: PublicKey,
748
- moderatorPda: PublicKey,
749
- numOptions: number = 2,
750
- ): Promise<{ altAddress: PublicKey }> {
751
- const provider = this.program.provider as AnchorProvider;
752
- const moderator = await this.fetchModerator(moderatorPda);
753
- const proposalId = moderator.proposalIdCounter;
754
- const [proposalPda] = this.deriveProposalPDA(moderatorPda, proposalId);
755
- const [vaultPda] = deriveVaultPDA(proposalPda, proposalId, this.vault.programId);
756
-
757
- const addresses: PublicKey[] = [
758
- // Programs
759
- this.programId,
760
- this.vault.programId,
761
- this.amm.programId,
762
- SystemProgram.programId,
763
- TOKEN_PROGRAM_ID,
764
- ASSOCIATED_TOKEN_PROGRAM_ID,
765
- // Core accounts
766
- moderatorPda,
767
- proposalPda,
768
- vaultPda,
769
- moderator.baseMint,
770
- moderator.quoteMint,
771
- FEE_AUTHORITY,
772
- // Vault token accounts
773
- getAssociatedTokenAddressSync(moderator.baseMint, vaultPda, true),
774
- getAssociatedTokenAddressSync(moderator.quoteMint, vaultPda, true),
775
- // Creator's base/quote ATAs
776
- getAssociatedTokenAddressSync(moderator.baseMint, creator),
777
- getAssociatedTokenAddressSync(moderator.quoteMint, creator),
778
- ];
779
-
780
- // Per-option accounts
781
- for (let i = 0; i < numOptions; i++) {
782
- const [condBaseMint] = deriveConditionalMint(vaultPda, VaultType.Base, i, this.vault.programId);
783
- const [condQuoteMint] = deriveConditionalMint(vaultPda, VaultType.Quote, i, this.vault.programId);
784
- const [pool] = derivePoolPDA(proposalPda, condQuoteMint, condBaseMint, this.amm.programId);
785
- const [reserveA] = deriveReservePDA(pool, condQuoteMint, this.amm.programId);
786
- const [reserveB] = deriveReservePDA(pool, condBaseMint, this.amm.programId);
787
- const [feeVault] = deriveFeeVaultPDA(pool, this.amm.programId);
788
-
789
- addresses.push(
790
- condBaseMint,
791
- condQuoteMint,
792
- pool,
793
- reserveA,
794
- reserveB,
795
- feeVault,
796
- // Creator's conditional token ATAs
797
- getAssociatedTokenAddressSync(condBaseMint, creator),
798
- getAssociatedTokenAddressSync(condQuoteMint, creator),
799
- );
800
- }
801
-
802
- // Get the most recent slot using "finalized" commitment for stability
803
- const slot = await provider.connection.getSlot("finalized");
804
-
805
- const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({
806
- authority: creator,
807
- payer: creator,
808
- recentSlot: slot,
809
- });
810
-
811
- // Send create transaction immediately, skip preflight to avoid slot timing issues
812
- const createTx = new Transaction().add(createIx);
813
- createTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash;
814
- createTx.feePayer = creator;
815
- const signedTx = await provider.wallet.signTransaction(createTx);
816
- const sig = await provider.connection.sendRawTransaction(signedTx.serialize(), {
817
- skipPreflight: true,
818
- });
819
- await provider.connection.confirmTransaction(sig, "confirmed");
820
-
821
- // Split addresses into chunks to avoid transaction size limits
822
- // Each address is 32 bytes, ~20 addresses per extend instruction is safe
823
- // Use skipPreflight to avoid race condition where simulation sees stale state
824
- // before previous extend has propagated (same pattern as CREATE above)
825
- const CHUNK_SIZE = 20;
826
- for (let i = 0; i < addresses.length; i += CHUNK_SIZE) {
827
- const chunk = addresses.slice(i, i + CHUNK_SIZE);
828
- const extendIx = AddressLookupTableProgram.extendLookupTable({
829
- payer: creator,
830
- authority: creator,
831
- lookupTable: altAddress,
832
- addresses: chunk,
833
- });
834
- const extendTx = new Transaction().add(extendIx);
835
- extendTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash;
836
- extendTx.feePayer = creator;
837
- const signedExtendTx = await provider.wallet.signTransaction(extendTx);
838
- const extendSig = await provider.connection.sendRawTransaction(signedExtendTx.serialize(), {
839
- skipPreflight: true,
840
- });
841
- await provider.connection.confirmTransaction(extendSig, "confirmed");
842
- }
843
-
844
- return { altAddress };
845
- }
846
-
847
- async fetchALT(altAddress: PublicKey): Promise<AddressLookupTableAccount> {
848
- const alt = await this.program.provider.connection.getAddressLookupTable(altAddress);
849
- if (!alt.value) {
850
- throw new Error("ALT not found");
851
- }
852
- return alt.value;
853
- }
854
-
855
- async buildVersionedTx(
856
- payer: PublicKey,
857
- instructions: TransactionInstruction[],
858
- altAddress: PublicKey,
859
- ): Promise<VersionedTransaction> {
860
- const provider = this.program.provider as AnchorProvider;
861
- const alt = await this.fetchALT(altAddress);
862
- const { blockhash } = await provider.connection.getLatestBlockhash();
863
- return this.buildVersionedTxWithALT(payer, instructions, alt, blockhash);
864
- }
865
-
866
- buildVersionedTxWithALT(
867
- payer: PublicKey,
868
- instructions: TransactionInstruction[],
869
- alt: AddressLookupTableAccount,
870
- blockhash: string,
871
- ): VersionedTransaction {
872
- const message = new TransactionMessage({
873
- payerKey: payer,
874
- recentBlockhash: blockhash,
875
- instructions,
876
- }).compileToV0Message([alt]);
877
-
878
- return new VersionedTransaction(message);
879
- }
880
-
881
- /* DAO Methods */
882
-
883
- deriveMintCreateKeyPDA(daoPda: PublicKey, name: string): [PublicKey, number] {
884
- return deriveMintCreateKeyPDA(daoPda, name, this.programId);
885
- }
886
-
887
- private async fetchSquadsProgramConfig() {
888
- const [programConfigPda] = multisig.getProgramConfigPda({
889
- programId: SQUADS_PROGRAM_ID,
890
- });
891
- const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress(
892
- this.program.provider.connection,
893
- programConfigPda
894
- );
895
- return {
896
- programConfig: programConfigPda,
897
- programConfigTreasury: programConfig.treasury,
898
- };
899
- }
900
-
901
- private deriveMultisigPda(createKey: PublicKey): PublicKey {
902
- const [multisigPda] = multisig.getMultisigPda({
903
- createKey,
904
- programId: SQUADS_PROGRAM_ID,
905
- });
906
- return multisigPda;
907
- }
908
-
909
- async initializeParentDAO(
910
- admin: PublicKey,
911
- parentAdmin: PublicKey,
912
- name: string,
913
- baseMint: PublicKey,
914
- quoteMint: PublicKey,
915
- treasuryCosigner: PublicKey,
916
- pool: PublicKey,
917
- poolType: PoolType,
918
- options?: TxOptions
919
- ) {
920
- const [daoPda] = this.deriveDAOPDA(name);
921
- const [moderatorPda] = this.deriveModeratorPDA(name);
922
- const [mintCreateKeyPda] = this.deriveMintCreateKeyPDA(daoPda, name);
923
-
924
- // Derive Squads accounts (single RPC call)
925
- const squadsConfig = await this.fetchSquadsProgramConfig();
926
- const treasuryMultisigPda = this.deriveMultisigPda(daoPda);
927
- const mintMultisigPda = this.deriveMultisigPda(mintCreateKeyPda);
928
-
929
- const builder = initializeParentDAO(
930
- this.program,
931
- admin,
932
- parentAdmin,
933
- daoPda,
934
- moderatorPda,
935
- baseMint,
936
- quoteMint,
937
- squadsConfig.programConfig,
938
- squadsConfig.programConfigTreasury,
939
- treasuryMultisigPda,
940
- mintMultisigPda,
941
- mintCreateKeyPda,
942
- SQUADS_PROGRAM_ID,
943
- name,
944
- treasuryCosigner,
945
- pool,
946
- poolType
947
- ).preInstructions(this.maybeAddComputeBudget(options));
948
-
949
- return {
950
- builder,
951
- daoPda,
952
- moderatorPda,
953
- treasuryMultisig: treasuryMultisigPda,
954
- mintMultisig: mintMultisigPda,
955
- };
956
- }
957
-
958
- async initializeChildDAO(
959
- admin: PublicKey,
960
- parentAdmin: PublicKey,
961
- parentDaoName: string,
962
- name: string,
963
- tokenMint: PublicKey,
964
- treasuryCosigner: PublicKey,
965
- options?: TxOptions
966
- ) {
967
- const [daoPda] = this.deriveDAOPDA(name);
968
- const [parentDaoPda] = this.deriveDAOPDA(parentDaoName);
969
- const [mintCreateKeyPda] = this.deriveMintCreateKeyPDA(daoPda, name);
970
-
971
- // Derive Squads accounts (single RPC call)
972
- const squadsConfig = await this.fetchSquadsProgramConfig();
973
- const treasuryMultisigPda = this.deriveMultisigPda(daoPda);
974
- const mintMultisigPda = this.deriveMultisigPda(mintCreateKeyPda);
975
-
976
- const builder = initializeChildDAO(
977
- this.program,
978
- admin,
979
- parentAdmin,
980
- daoPda,
981
- parentDaoPda,
982
- tokenMint,
983
- squadsConfig.programConfig,
984
- squadsConfig.programConfigTreasury,
985
- treasuryMultisigPda,
986
- mintMultisigPda,
987
- mintCreateKeyPda,
988
- SQUADS_PROGRAM_ID,
989
- name,
990
- treasuryCosigner
991
- ).preInstructions(this.maybeAddComputeBudget(options));
992
-
993
- return {
994
- builder,
995
- daoPda,
996
- parentDaoPda,
997
- treasuryMultisig: treasuryMultisigPda,
998
- mintMultisig: mintMultisigPda,
999
- };
1000
- }
1001
-
1002
- async upgradeDAO(
1003
- admin: PublicKey,
1004
- parentAdmin: PublicKey,
1005
- daoName: string,
1006
- parentDaoName: string,
1007
- baseMint: PublicKey,
1008
- quoteMint: PublicKey,
1009
- pool: PublicKey,
1010
- poolType: PoolType,
1011
- options?: TxOptions
1012
- ) {
1013
- const [daoPda] = this.deriveDAOPDA(daoName);
1014
- const [parentDaoPda] = this.deriveDAOPDA(parentDaoName);
1015
- const [moderatorPda] = this.deriveModeratorPDA(daoName);
1016
-
1017
- const builder = upgradeDAO(
1018
- this.program,
1019
- admin,
1020
- parentAdmin,
1021
- daoPda,
1022
- parentDaoPda,
1023
- moderatorPda,
1024
- baseMint,
1025
- quoteMint,
1026
- pool,
1027
- poolType
1028
- ).preInstructions(this.maybeAddComputeBudget(options));
1029
-
1030
- return { builder, daoPda, moderatorPda };
1031
- }
1032
- }