@zcomb/programs-sdk 1.7.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.
- package/dist/futarchy/client.d.ts +113 -1
- package/dist/futarchy/client.js +13 -3
- package/dist/futarchy/instructions.d.ts +109 -1
- package/dist/futarchy/instructions.js +17 -3
- package/dist/generated/idls/futarchy.json +112 -0
- package/dist/generated/types/futarchy.d.ts +112 -0
- package/dist/generated/types/futarchy.js +1 -1
- package/package.json +1 -2
- package/src/amm/client.ts +0 -485
- package/src/amm/constants.ts +0 -31
- package/src/amm/index.ts +0 -5
- package/src/amm/instructions.ts +0 -139
- package/src/amm/types.ts +0 -62
- package/src/amm/utils.ts +0 -263
- package/src/futarchy/client.ts +0 -1100
- package/src/futarchy/constants.ts +0 -28
- package/src/futarchy/index.ts +0 -5
- package/src/futarchy/instructions.ts +0 -235
- package/src/futarchy/types.ts +0 -54
- package/src/futarchy/utils.ts +0 -108
- package/src/generated/idls/amm.json +0 -1252
- package/src/generated/idls/futarchy.json +0 -1763
- package/src/generated/idls/index.ts +0 -4
- package/src/generated/idls/svault.json +0 -2228
- package/src/generated/idls/vault.json +0 -1501
- package/src/generated/types/amm.ts +0 -1258
- package/src/generated/types/futarchy.ts +0 -1769
- package/src/generated/types/index.ts +0 -4
- package/src/generated/types/svault.ts +0 -2234
- package/src/generated/types/vault.ts +0 -1507
- package/src/index.ts +0 -163
- package/src/svault/client.ts +0 -401
- package/src/svault/constants.ts +0 -23
- package/src/svault/index.ts +0 -5
- package/src/svault/instructions.ts +0 -258
- package/src/svault/types.ts +0 -45
- package/src/svault/utils.ts +0 -145
- package/src/utils.ts +0 -41
- package/src/vault/client.ts +0 -333
- package/src/vault/constants.ts +0 -23
- package/src/vault/index.ts +0 -5
- package/src/vault/instructions.ts +0 -170
- package/src/vault/types.ts +0 -54
- package/src/vault/utils.ts +0 -70
package/src/futarchy/client.ts
DELETED
|
@@ -1,1100 +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
|
-
const { blockhash: createBlockhash, lastValidBlockHeight: createLastValidBlockHeight } =
|
|
610
|
-
await provider.connection.getLatestBlockhash('confirmed');
|
|
611
|
-
createTx.recentBlockhash = createBlockhash;
|
|
612
|
-
createTx.feePayer = creator;
|
|
613
|
-
const signedTx = await provider.wallet.signTransaction(createTx);
|
|
614
|
-
const sig = await provider.connection.sendRawTransaction(signedTx.serialize(), {
|
|
615
|
-
skipPreflight: true,
|
|
616
|
-
});
|
|
617
|
-
await provider.connection.confirmTransaction({
|
|
618
|
-
signature: sig,
|
|
619
|
-
blockhash: createBlockhash,
|
|
620
|
-
lastValidBlockHeight: createLastValidBlockHeight,
|
|
621
|
-
}, "confirmed");
|
|
622
|
-
|
|
623
|
-
// Extend ALT with addresses
|
|
624
|
-
// Use skipPreflight to avoid race condition where simulation sees stale state
|
|
625
|
-
// before previous extend has propagated (same pattern as CREATE above)
|
|
626
|
-
const CHUNK_SIZE = 20;
|
|
627
|
-
for (let i = 0; i < addresses.length; i += CHUNK_SIZE) {
|
|
628
|
-
const chunk = addresses.slice(i, i + CHUNK_SIZE);
|
|
629
|
-
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
|
630
|
-
payer: creator,
|
|
631
|
-
authority: creator,
|
|
632
|
-
lookupTable: altAddress,
|
|
633
|
-
addresses: chunk,
|
|
634
|
-
});
|
|
635
|
-
const extendTx = new Transaction().add(extendIx);
|
|
636
|
-
const { blockhash: extendBlockhash, lastValidBlockHeight: extendLastValidBlockHeight } =
|
|
637
|
-
await provider.connection.getLatestBlockhash('confirmed');
|
|
638
|
-
extendTx.recentBlockhash = extendBlockhash;
|
|
639
|
-
extendTx.feePayer = creator;
|
|
640
|
-
const signedExtendTx = await provider.wallet.signTransaction(extendTx);
|
|
641
|
-
const extendSig = await provider.connection.sendRawTransaction(signedExtendTx.serialize(), {
|
|
642
|
-
skipPreflight: true,
|
|
643
|
-
});
|
|
644
|
-
await provider.connection.confirmTransaction({
|
|
645
|
-
signature: extendSig,
|
|
646
|
-
blockhash: extendBlockhash,
|
|
647
|
-
lastValidBlockHeight: extendLastValidBlockHeight,
|
|
648
|
-
}, "confirmed");
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return { altAddress };
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Builds a versioned transaction for redeeming liquidity with ALT.
|
|
656
|
-
* Required for proposals with 3+ options to avoid exceeding transaction size limits.
|
|
657
|
-
*
|
|
658
|
-
* @param creator - The user redeeming
|
|
659
|
-
* @param proposalPda - The proposal PDA
|
|
660
|
-
* @param altAddress - Optional ALT address (will be created if not provided for 3+ options)
|
|
661
|
-
* @returns Unsigned versioned transaction, ALT address, number of options, and blockhash info for confirmation
|
|
662
|
-
*/
|
|
663
|
-
async redeemLiquidityVersioned(
|
|
664
|
-
creator: PublicKey,
|
|
665
|
-
proposalPda: PublicKey,
|
|
666
|
-
altAddress?: PublicKey,
|
|
667
|
-
): Promise<{
|
|
668
|
-
versionedTx: VersionedTransaction;
|
|
669
|
-
altAddress: PublicKey;
|
|
670
|
-
numOptions: number;
|
|
671
|
-
blockhash: string;
|
|
672
|
-
lastValidBlockHeight: number;
|
|
673
|
-
}> {
|
|
674
|
-
const provider = this.program.provider as AnchorProvider;
|
|
675
|
-
const { builder, numOptions } = await this.redeemLiquidity(creator, proposalPda);
|
|
676
|
-
|
|
677
|
-
// Create ALT if not provided and needed
|
|
678
|
-
let altPubkey = altAddress;
|
|
679
|
-
let verifiedALT: AddressLookupTableAccount | null = null;
|
|
680
|
-
|
|
681
|
-
if (!altPubkey && numOptions >= 3) {
|
|
682
|
-
console.log(` Creating redemption ALT for ${numOptions} options...`);
|
|
683
|
-
const result = await this.createRedemptionALT(creator, proposalPda);
|
|
684
|
-
altPubkey = result.altAddress;
|
|
685
|
-
console.log(` ✓ Redemption ALT created: ${altPubkey.toBase58()}`);
|
|
686
|
-
|
|
687
|
-
// Wait for ALT to be fully available with all addresses
|
|
688
|
-
// Use longer delays after extending to ensure propagation
|
|
689
|
-
console.log(` Waiting for ALT propagation...`);
|
|
690
|
-
const expectedAddresses = 18 + (numOptions * 4); // Base accounts + per-option accounts
|
|
691
|
-
let attempts = 0;
|
|
692
|
-
|
|
693
|
-
// Initial delay after extension
|
|
694
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
695
|
-
|
|
696
|
-
while (attempts < 30) {
|
|
697
|
-
const altAccount = await provider.connection.getAddressLookupTable(altPubkey, {
|
|
698
|
-
commitment: 'confirmed',
|
|
699
|
-
});
|
|
700
|
-
if (altAccount.value) {
|
|
701
|
-
const addressCount = altAccount.value.state.addresses.length;
|
|
702
|
-
console.log(` Attempt ${attempts + 1}: ALT has ${addressCount}/${expectedAddresses} addresses`);
|
|
703
|
-
if (addressCount >= expectedAddresses) {
|
|
704
|
-
verifiedALT = altAccount.value;
|
|
705
|
-
console.log(` ✓ ALT verified with ${addressCount} addresses`);
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
710
|
-
attempts++;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (!verifiedALT) {
|
|
714
|
-
throw new Error(`ALT failed to populate with expected ${expectedAddresses} addresses after ${attempts} attempts`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (!altPubkey) {
|
|
719
|
-
throw new Error("ALT address required for multi-option redemption");
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Fetch ALT if we don't have it verified already
|
|
723
|
-
if (!verifiedALT) {
|
|
724
|
-
verifiedALT = await this.fetchALT(altPubkey);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Build instruction
|
|
728
|
-
const instruction = await builder.instruction();
|
|
729
|
-
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 });
|
|
730
|
-
|
|
731
|
-
// Get fresh blockhash with lastValidBlockHeight for confirmation tracking
|
|
732
|
-
const { blockhash, lastValidBlockHeight } = await provider.connection.getLatestBlockhash('confirmed');
|
|
733
|
-
|
|
734
|
-
// Build versioned transaction using the verified ALT (no re-fetch)
|
|
735
|
-
const versionedTx = this.buildVersionedTxWithALT(
|
|
736
|
-
creator,
|
|
737
|
-
[computeBudgetIx, instruction],
|
|
738
|
-
verifiedALT,
|
|
739
|
-
blockhash,
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
// Return the versioned transaction along with blockhash info for proper confirmation
|
|
743
|
-
// This allows the caller to use their own signing mechanism (e.g., keypair.sign)
|
|
744
|
-
return { versionedTx, altAddress: altPubkey, numOptions, blockhash, lastValidBlockHeight };
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Helper to send a signed versioned transaction with robust confirmation handling.
|
|
749
|
-
* Uses blockhash-based confirmation to properly detect transaction expiration
|
|
750
|
-
* instead of relying on a fixed timeout.
|
|
751
|
-
*
|
|
752
|
-
* @param signedTx - The signed versioned transaction to send
|
|
753
|
-
* @param confirmationInfo - Optional blockhash info from when the transaction was built.
|
|
754
|
-
* If not provided, a fresh blockhash will be fetched (less accurate).
|
|
755
|
-
*/
|
|
756
|
-
async sendVersionedTransaction(
|
|
757
|
-
signedTx: VersionedTransaction,
|
|
758
|
-
confirmationInfo?: { blockhash: string; lastValidBlockHeight: number },
|
|
759
|
-
): Promise<string> {
|
|
760
|
-
const provider = this.program.provider as AnchorProvider;
|
|
761
|
-
|
|
762
|
-
// Use provided blockhash info or fetch fresh one
|
|
763
|
-
// Using the original blockhash is more accurate for detecting expiration
|
|
764
|
-
let blockhash: string;
|
|
765
|
-
let lastValidBlockHeight: number;
|
|
766
|
-
|
|
767
|
-
if (confirmationInfo) {
|
|
768
|
-
blockhash = confirmationInfo.blockhash;
|
|
769
|
-
lastValidBlockHeight = confirmationInfo.lastValidBlockHeight;
|
|
770
|
-
} else {
|
|
771
|
-
// Fallback: get fresh blockhash (may wait longer than necessary if tx already expired)
|
|
772
|
-
const latestBlockhash = await provider.connection.getLatestBlockhash('confirmed');
|
|
773
|
-
blockhash = latestBlockhash.blockhash;
|
|
774
|
-
lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const signature = await provider.connection.sendTransaction(signedTx, {
|
|
778
|
-
skipPreflight: false,
|
|
779
|
-
preflightCommitment: 'confirmed',
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
// Use blockhash-based confirmation which waits until either:
|
|
783
|
-
// 1. Transaction is confirmed
|
|
784
|
-
// 2. Blockhash expires (lastValidBlockHeight passed)
|
|
785
|
-
// This is more robust than a fixed timeout on congested networks
|
|
786
|
-
await provider.connection.confirmTransaction({
|
|
787
|
-
signature,
|
|
788
|
-
blockhash,
|
|
789
|
-
lastValidBlockHeight,
|
|
790
|
-
}, 'confirmed');
|
|
791
|
-
|
|
792
|
-
return signature;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/* Address Lookup Table */
|
|
796
|
-
|
|
797
|
-
async createProposalALT(
|
|
798
|
-
creator: PublicKey,
|
|
799
|
-
moderatorPda: PublicKey,
|
|
800
|
-
numOptions: number = 2,
|
|
801
|
-
): Promise<{ altAddress: PublicKey }> {
|
|
802
|
-
const provider = this.program.provider as AnchorProvider;
|
|
803
|
-
const moderator = await this.fetchModerator(moderatorPda);
|
|
804
|
-
const proposalId = moderator.proposalIdCounter;
|
|
805
|
-
const [proposalPda] = this.deriveProposalPDA(moderatorPda, proposalId);
|
|
806
|
-
const [vaultPda] = deriveVaultPDA(proposalPda, proposalId, this.vault.programId);
|
|
807
|
-
|
|
808
|
-
const addresses: PublicKey[] = [
|
|
809
|
-
// Programs
|
|
810
|
-
this.programId,
|
|
811
|
-
this.vault.programId,
|
|
812
|
-
this.amm.programId,
|
|
813
|
-
SystemProgram.programId,
|
|
814
|
-
TOKEN_PROGRAM_ID,
|
|
815
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
816
|
-
// Core accounts
|
|
817
|
-
moderatorPda,
|
|
818
|
-
proposalPda,
|
|
819
|
-
vaultPda,
|
|
820
|
-
moderator.baseMint,
|
|
821
|
-
moderator.quoteMint,
|
|
822
|
-
FEE_AUTHORITY,
|
|
823
|
-
// Vault token accounts
|
|
824
|
-
getAssociatedTokenAddressSync(moderator.baseMint, vaultPda, true),
|
|
825
|
-
getAssociatedTokenAddressSync(moderator.quoteMint, vaultPda, true),
|
|
826
|
-
// Creator's base/quote ATAs
|
|
827
|
-
getAssociatedTokenAddressSync(moderator.baseMint, creator),
|
|
828
|
-
getAssociatedTokenAddressSync(moderator.quoteMint, creator),
|
|
829
|
-
];
|
|
830
|
-
|
|
831
|
-
// Per-option accounts
|
|
832
|
-
for (let i = 0; i < numOptions; i++) {
|
|
833
|
-
const [condBaseMint] = deriveConditionalMint(vaultPda, VaultType.Base, i, this.vault.programId);
|
|
834
|
-
const [condQuoteMint] = deriveConditionalMint(vaultPda, VaultType.Quote, i, this.vault.programId);
|
|
835
|
-
const [pool] = derivePoolPDA(proposalPda, condQuoteMint, condBaseMint, this.amm.programId);
|
|
836
|
-
const [reserveA] = deriveReservePDA(pool, condQuoteMint, this.amm.programId);
|
|
837
|
-
const [reserveB] = deriveReservePDA(pool, condBaseMint, this.amm.programId);
|
|
838
|
-
const [feeVault] = deriveFeeVaultPDA(pool, this.amm.programId);
|
|
839
|
-
|
|
840
|
-
addresses.push(
|
|
841
|
-
condBaseMint,
|
|
842
|
-
condQuoteMint,
|
|
843
|
-
pool,
|
|
844
|
-
reserveA,
|
|
845
|
-
reserveB,
|
|
846
|
-
feeVault,
|
|
847
|
-
// Creator's conditional token ATAs
|
|
848
|
-
getAssociatedTokenAddressSync(condBaseMint, creator),
|
|
849
|
-
getAssociatedTokenAddressSync(condQuoteMint, creator),
|
|
850
|
-
);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Get the most recent slot using "finalized" commitment for stability
|
|
854
|
-
const slot = await provider.connection.getSlot("finalized");
|
|
855
|
-
|
|
856
|
-
const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({
|
|
857
|
-
authority: creator,
|
|
858
|
-
payer: creator,
|
|
859
|
-
recentSlot: slot,
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
// Send create transaction immediately, skip preflight to avoid slot timing issues
|
|
863
|
-
const createTx = new Transaction().add(createIx);
|
|
864
|
-
const { blockhash: createBlockhash, lastValidBlockHeight: createLastValidBlockHeight } =
|
|
865
|
-
await provider.connection.getLatestBlockhash('confirmed');
|
|
866
|
-
createTx.recentBlockhash = createBlockhash;
|
|
867
|
-
createTx.feePayer = creator;
|
|
868
|
-
const signedTx = await provider.wallet.signTransaction(createTx);
|
|
869
|
-
const sig = await provider.connection.sendRawTransaction(signedTx.serialize(), {
|
|
870
|
-
skipPreflight: true,
|
|
871
|
-
});
|
|
872
|
-
await provider.connection.confirmTransaction({
|
|
873
|
-
signature: sig,
|
|
874
|
-
blockhash: createBlockhash,
|
|
875
|
-
lastValidBlockHeight: createLastValidBlockHeight,
|
|
876
|
-
}, "confirmed");
|
|
877
|
-
|
|
878
|
-
// Split addresses into chunks to avoid transaction size limits
|
|
879
|
-
// Each address is 32 bytes, ~20 addresses per extend instruction is safe
|
|
880
|
-
// Use skipPreflight to avoid race condition where simulation sees stale state
|
|
881
|
-
// before previous extend has propagated (same pattern as CREATE above)
|
|
882
|
-
const CHUNK_SIZE = 20;
|
|
883
|
-
for (let i = 0; i < addresses.length; i += CHUNK_SIZE) {
|
|
884
|
-
const chunk = addresses.slice(i, i + CHUNK_SIZE);
|
|
885
|
-
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
|
886
|
-
payer: creator,
|
|
887
|
-
authority: creator,
|
|
888
|
-
lookupTable: altAddress,
|
|
889
|
-
addresses: chunk,
|
|
890
|
-
});
|
|
891
|
-
const extendTx = new Transaction().add(extendIx);
|
|
892
|
-
const { blockhash: extendBlockhash, lastValidBlockHeight: extendLastValidBlockHeight } =
|
|
893
|
-
await provider.connection.getLatestBlockhash('confirmed');
|
|
894
|
-
extendTx.recentBlockhash = extendBlockhash;
|
|
895
|
-
extendTx.feePayer = creator;
|
|
896
|
-
const signedExtendTx = await provider.wallet.signTransaction(extendTx);
|
|
897
|
-
const extendSig = await provider.connection.sendRawTransaction(signedExtendTx.serialize(), {
|
|
898
|
-
skipPreflight: true,
|
|
899
|
-
});
|
|
900
|
-
await provider.connection.confirmTransaction({
|
|
901
|
-
signature: extendSig,
|
|
902
|
-
blockhash: extendBlockhash,
|
|
903
|
-
lastValidBlockHeight: extendLastValidBlockHeight,
|
|
904
|
-
}, "confirmed");
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return { altAddress };
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
async fetchALT(altAddress: PublicKey): Promise<AddressLookupTableAccount> {
|
|
911
|
-
const alt = await this.program.provider.connection.getAddressLookupTable(altAddress);
|
|
912
|
-
if (!alt.value) {
|
|
913
|
-
throw new Error("ALT not found");
|
|
914
|
-
}
|
|
915
|
-
return alt.value;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
async buildVersionedTx(
|
|
919
|
-
payer: PublicKey,
|
|
920
|
-
instructions: TransactionInstruction[],
|
|
921
|
-
altAddress: PublicKey,
|
|
922
|
-
): Promise<{
|
|
923
|
-
versionedTx: VersionedTransaction;
|
|
924
|
-
blockhash: string;
|
|
925
|
-
lastValidBlockHeight: number;
|
|
926
|
-
}> {
|
|
927
|
-
const provider = this.program.provider as AnchorProvider;
|
|
928
|
-
const alt = await this.fetchALT(altAddress);
|
|
929
|
-
const { blockhash, lastValidBlockHeight } = await provider.connection.getLatestBlockhash('confirmed');
|
|
930
|
-
const versionedTx = this.buildVersionedTxWithALT(payer, instructions, alt, blockhash);
|
|
931
|
-
return { versionedTx, blockhash, lastValidBlockHeight };
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
buildVersionedTxWithALT(
|
|
935
|
-
payer: PublicKey,
|
|
936
|
-
instructions: TransactionInstruction[],
|
|
937
|
-
alt: AddressLookupTableAccount,
|
|
938
|
-
blockhash: string,
|
|
939
|
-
): VersionedTransaction {
|
|
940
|
-
const message = new TransactionMessage({
|
|
941
|
-
payerKey: payer,
|
|
942
|
-
recentBlockhash: blockhash,
|
|
943
|
-
instructions,
|
|
944
|
-
}).compileToV0Message([alt]);
|
|
945
|
-
|
|
946
|
-
return new VersionedTransaction(message);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/* DAO Methods */
|
|
950
|
-
|
|
951
|
-
deriveMintCreateKeyPDA(daoPda: PublicKey, name: string): [PublicKey, number] {
|
|
952
|
-
return deriveMintCreateKeyPDA(daoPda, name, this.programId);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
private async fetchSquadsProgramConfig() {
|
|
956
|
-
const [programConfigPda] = multisig.getProgramConfigPda({
|
|
957
|
-
programId: SQUADS_PROGRAM_ID,
|
|
958
|
-
});
|
|
959
|
-
const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress(
|
|
960
|
-
this.program.provider.connection,
|
|
961
|
-
programConfigPda
|
|
962
|
-
);
|
|
963
|
-
return {
|
|
964
|
-
programConfig: programConfigPda,
|
|
965
|
-
programConfigTreasury: programConfig.treasury,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
private deriveMultisigPda(createKey: PublicKey): PublicKey {
|
|
970
|
-
const [multisigPda] = multisig.getMultisigPda({
|
|
971
|
-
createKey,
|
|
972
|
-
programId: SQUADS_PROGRAM_ID,
|
|
973
|
-
});
|
|
974
|
-
return multisigPda;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async initializeParentDAO(
|
|
978
|
-
admin: PublicKey,
|
|
979
|
-
parentAdmin: PublicKey,
|
|
980
|
-
name: string,
|
|
981
|
-
baseMint: PublicKey,
|
|
982
|
-
quoteMint: PublicKey,
|
|
983
|
-
treasuryCosigner: PublicKey,
|
|
984
|
-
pool: PublicKey,
|
|
985
|
-
poolType: PoolType,
|
|
986
|
-
options?: TxOptions
|
|
987
|
-
) {
|
|
988
|
-
const [daoPda] = this.deriveDAOPDA(name);
|
|
989
|
-
const [moderatorPda] = this.deriveModeratorPDA(name);
|
|
990
|
-
const [mintCreateKeyPda] = this.deriveMintCreateKeyPDA(daoPda, name);
|
|
991
|
-
|
|
992
|
-
// Derive Squads accounts (single RPC call)
|
|
993
|
-
const squadsConfig = await this.fetchSquadsProgramConfig();
|
|
994
|
-
const treasuryMultisigPda = this.deriveMultisigPda(daoPda);
|
|
995
|
-
const mintMultisigPda = this.deriveMultisigPda(mintCreateKeyPda);
|
|
996
|
-
|
|
997
|
-
const builder = initializeParentDAO(
|
|
998
|
-
this.program,
|
|
999
|
-
admin,
|
|
1000
|
-
parentAdmin,
|
|
1001
|
-
daoPda,
|
|
1002
|
-
moderatorPda,
|
|
1003
|
-
baseMint,
|
|
1004
|
-
quoteMint,
|
|
1005
|
-
squadsConfig.programConfig,
|
|
1006
|
-
squadsConfig.programConfigTreasury,
|
|
1007
|
-
treasuryMultisigPda,
|
|
1008
|
-
mintMultisigPda,
|
|
1009
|
-
mintCreateKeyPda,
|
|
1010
|
-
SQUADS_PROGRAM_ID,
|
|
1011
|
-
name,
|
|
1012
|
-
treasuryCosigner,
|
|
1013
|
-
pool,
|
|
1014
|
-
poolType
|
|
1015
|
-
).preInstructions(this.maybeAddComputeBudget(options));
|
|
1016
|
-
|
|
1017
|
-
return {
|
|
1018
|
-
builder,
|
|
1019
|
-
daoPda,
|
|
1020
|
-
moderatorPda,
|
|
1021
|
-
treasuryMultisig: treasuryMultisigPda,
|
|
1022
|
-
mintMultisig: mintMultisigPda,
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
async initializeChildDAO(
|
|
1027
|
-
admin: PublicKey,
|
|
1028
|
-
parentAdmin: PublicKey,
|
|
1029
|
-
parentDaoName: string,
|
|
1030
|
-
name: string,
|
|
1031
|
-
tokenMint: PublicKey,
|
|
1032
|
-
treasuryCosigner: PublicKey,
|
|
1033
|
-
options?: TxOptions
|
|
1034
|
-
) {
|
|
1035
|
-
const [daoPda] = this.deriveDAOPDA(name);
|
|
1036
|
-
const [parentDaoPda] = this.deriveDAOPDA(parentDaoName);
|
|
1037
|
-
const [mintCreateKeyPda] = this.deriveMintCreateKeyPDA(daoPda, name);
|
|
1038
|
-
|
|
1039
|
-
// Derive Squads accounts (single RPC call)
|
|
1040
|
-
const squadsConfig = await this.fetchSquadsProgramConfig();
|
|
1041
|
-
const treasuryMultisigPda = this.deriveMultisigPda(daoPda);
|
|
1042
|
-
const mintMultisigPda = this.deriveMultisigPda(mintCreateKeyPda);
|
|
1043
|
-
|
|
1044
|
-
const builder = initializeChildDAO(
|
|
1045
|
-
this.program,
|
|
1046
|
-
admin,
|
|
1047
|
-
parentAdmin,
|
|
1048
|
-
daoPda,
|
|
1049
|
-
parentDaoPda,
|
|
1050
|
-
tokenMint,
|
|
1051
|
-
squadsConfig.programConfig,
|
|
1052
|
-
squadsConfig.programConfigTreasury,
|
|
1053
|
-
treasuryMultisigPda,
|
|
1054
|
-
mintMultisigPda,
|
|
1055
|
-
mintCreateKeyPda,
|
|
1056
|
-
SQUADS_PROGRAM_ID,
|
|
1057
|
-
name,
|
|
1058
|
-
treasuryCosigner
|
|
1059
|
-
).preInstructions(this.maybeAddComputeBudget(options));
|
|
1060
|
-
|
|
1061
|
-
return {
|
|
1062
|
-
builder,
|
|
1063
|
-
daoPda,
|
|
1064
|
-
parentDaoPda,
|
|
1065
|
-
treasuryMultisig: treasuryMultisigPda,
|
|
1066
|
-
mintMultisig: mintMultisigPda,
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
async upgradeDAO(
|
|
1071
|
-
admin: PublicKey,
|
|
1072
|
-
parentAdmin: PublicKey,
|
|
1073
|
-
daoName: string,
|
|
1074
|
-
parentDaoName: string,
|
|
1075
|
-
baseMint: PublicKey,
|
|
1076
|
-
quoteMint: PublicKey,
|
|
1077
|
-
pool: PublicKey,
|
|
1078
|
-
poolType: PoolType,
|
|
1079
|
-
options?: TxOptions
|
|
1080
|
-
) {
|
|
1081
|
-
const [daoPda] = this.deriveDAOPDA(daoName);
|
|
1082
|
-
const [parentDaoPda] = this.deriveDAOPDA(parentDaoName);
|
|
1083
|
-
const [moderatorPda] = this.deriveModeratorPDA(daoName);
|
|
1084
|
-
|
|
1085
|
-
const builder = upgradeDAO(
|
|
1086
|
-
this.program,
|
|
1087
|
-
admin,
|
|
1088
|
-
parentAdmin,
|
|
1089
|
-
daoPda,
|
|
1090
|
-
parentDaoPda,
|
|
1091
|
-
moderatorPda,
|
|
1092
|
-
baseMint,
|
|
1093
|
-
quoteMint,
|
|
1094
|
-
pool,
|
|
1095
|
-
poolType
|
|
1096
|
-
).preInstructions(this.maybeAddComputeBudget(options));
|
|
1097
|
-
|
|
1098
|
-
return { builder, daoPda, moderatorPda };
|
|
1099
|
-
}
|
|
1100
|
-
}
|