@zcomb/programs-sdk 1.2.0 → 1.4.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/amm/client.d.ts +1 -2
- package/dist/amm/client.js +6 -50
- package/dist/amm/types.d.ts +1 -0
- package/dist/amm/types.js +1 -1
- package/dist/futarchy/client.d.ts +51 -300
- package/dist/futarchy/client.js +209 -40
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/src/amm/client.ts +5 -61
- package/src/amm/types.ts +2 -1
- package/src/futarchy/client.ts +288 -57
- package/src/utils.ts +6 -0
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface TxOptions {
|
|
2
2
|
includeCuBudget?: boolean;
|
|
3
3
|
computeUnits?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Pre-create conditional token ATAs before launch to avoid exceeding
|
|
6
|
+
* Solana's 64 instruction trace limit. Default: true for 3+ options.
|
|
7
|
+
* Set to false if you've already created ATAs or want to manage them manually.
|
|
8
|
+
*/
|
|
9
|
+
ensureATAs?: boolean;
|
|
4
10
|
}
|
|
5
11
|
export declare function parseIdlBytes(value: string): Buffer;
|
|
6
12
|
export declare function getIdlConstant(idl: {
|
package/dist/utils.js
CHANGED
|
@@ -25,4 +25,4 @@ function getIdlConstant(idl, name) {
|
|
|
25
25
|
}
|
|
26
26
|
return constant.value;
|
|
27
27
|
}
|
|
28
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7R0FHRzs7QUFtQkgsc0NBR0M7QUFNRCx3Q0FTQztBQXRCRDs7O0dBR0c7QUFDSCxTQUFnQixhQUFhLENBQUMsS0FBYTtJQUN6QyxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBYSxDQUFDO0lBQzVDLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUM1QixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsU0FBZ0IsY0FBYyxDQUM1QixHQUEwRCxFQUMxRCxJQUFZO0lBRVosTUFBTSxRQUFRLEdBQUcsR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssSUFBSSxDQUFDLENBQUM7SUFDNUQsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ2QsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNuRCxDQUFDO0lBQ0QsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDO0FBQ3hCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKlxuICogU2hhcmVkIHV0aWxpdGllcyBmb3IgcGFyc2luZyBJREwgdmFsdWVzIGFuZCBjb21tb24gdHlwZXMuXG4gKiBVc2VkIGFjcm9zcyBwcm9ncmFtIFNES3MuXG4gKi9cblxuLyogVHJhbnNhY3Rpb24gT3B0aW9ucyAqL1xuXG5leHBvcnQgaW50ZXJmYWNlIFR4T3B0aW9ucyB7XG4gIGluY2x1ZGVDdUJ1ZGdldD86IGJvb2xlYW47ICAvLyBJbmNsdWRlIGNvbXB1dGUgYnVkZ2V0IGluc3RydWN0aW9uIChkZWZhdWx0OiB0cnVlKVxuICBjb21wdXRlVW5pdHM/OiBudW1iZXI7ICAgICAgLy8gT3ZlcnJpZGUgZGVmYXVsdCBjb21wdXRlIHVuaXRzXG4gIC8qKlxuICAgKiBQcmUtY3JlYXRlIGNvbmRpdGlvbmFsIHRva2VuIEFUQXMgYmVmb3JlIGxhdW5jaCB0byBhdm9pZCBleGNlZWRpbmdcbiAgICogU29sYW5hJ3MgNjQgaW5zdHJ1Y3Rpb24gdHJhY2UgbGltaXQuIERlZmF1bHQ6IHRydWUgZm9yIDMrIG9wdGlvbnMuXG4gICAqIFNldCB0byBmYWxzZSBpZiB5b3UndmUgYWxyZWFkeSBjcmVhdGVkIEFUQXMgb3Igd2FudCB0byBtYW5hZ2UgdGhlbSBtYW51YWxseS5cbiAgICovXG4gIGVuc3VyZUFUQXM/OiBib29sZWFuO1xufVxuXG4vKlxuICogUGFyc2UgSURMIGJ5dGVzIHZhbHVlIHN0cmluZyB0byBCdWZmZXIuXG4gKiBlLmcuLCBcIls5OSwgMTA5LCAxMDUsIDExMCwgMTE2XVwiIOKGkiBCdWZmZXIuZnJvbShbOTksIDEwOSwgMTA1LCAxMTAsIDExNl0pXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBwYXJzZUlkbEJ5dGVzKHZhbHVlOiBzdHJpbmcpOiBCdWZmZXIge1xuICBjb25zdCBieXRlcyA9IEpTT04ucGFyc2UodmFsdWUpIGFzIG51bWJlcltdO1xuICByZXR1cm4gQnVmZmVyLmZyb20oYnl0ZXMpO1xufVxuXG4vKlxuICogR2V0IGEgY29uc3RhbnQgZnJvbSBhbiBJREwgYnkgbmFtZS5cbiAqIFRocm93cyBpZiB0aGUgY29uc3RhbnQgaXMgbm90IGZvdW5kIChoZWxwcyBjYXRjaCBtaXNtYXRjaGVzIGFmdGVyIElETCBzeW5jKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdldElkbENvbnN0YW50KFxuICBpZGw6IHsgY29uc3RhbnRzOiBBcnJheTx7IG5hbWU6IHN0cmluZzsgdmFsdWU6IHN0cmluZyB9PiB9LFxuICBuYW1lOiBzdHJpbmdcbikge1xuICBjb25zdCBjb25zdGFudCA9IGlkbC5jb25zdGFudHMuZmluZCgoYykgPT4gYy5uYW1lID09PSBuYW1lKTtcbiAgaWYgKCFjb25zdGFudCkge1xuICAgIHRocm93IG5ldyBFcnJvcihgSURMIG1pc3NpbmcgY29uc3RhbnQ6ICR7bmFtZX1gKTtcbiAgfVxuICByZXR1cm4gY29uc3RhbnQudmFsdWU7XG59XG4iXX0=
|
package/package.json
CHANGED
package/src/amm/client.ts
CHANGED
|
@@ -330,7 +330,7 @@ export class AMMClient {
|
|
|
330
330
|
minOutputAmount: BN | number,
|
|
331
331
|
options?: AmmActionOptions
|
|
332
332
|
) {
|
|
333
|
-
const {
|
|
333
|
+
const { autoCreateTokenAccounts = true, includeCuBudget = true, computeUnits } = options ?? {};
|
|
334
334
|
|
|
335
335
|
const pool = await this.fetchPool(poolPda);
|
|
336
336
|
const [reserveA] = this.deriveReservePDA(poolPda, pool.mintA);
|
|
@@ -340,7 +340,6 @@ export class AMMClient {
|
|
|
340
340
|
const traderAccountB = getAssociatedTokenAddressSync(pool.mintB, trader);
|
|
341
341
|
|
|
342
342
|
const preIxs: TransactionInstruction[] = [];
|
|
343
|
-
const postIxs: TransactionInstruction[] = [];
|
|
344
343
|
|
|
345
344
|
if (includeCuBudget) {
|
|
346
345
|
preIxs.push(
|
|
@@ -350,13 +349,7 @@ export class AMMClient {
|
|
|
350
349
|
);
|
|
351
350
|
}
|
|
352
351
|
|
|
353
|
-
|
|
354
|
-
const outputMint = swapAToB ? pool.mintB : pool.mintA;
|
|
355
|
-
const inputAta = swapAToB ? traderAccountA : traderAccountB;
|
|
356
|
-
const outputAta = swapAToB ? traderAccountB : traderAccountA;
|
|
357
|
-
|
|
358
|
-
if (autoWrapUnwrap) {
|
|
359
|
-
// Create ATAs idempotently
|
|
352
|
+
if (autoCreateTokenAccounts) {
|
|
360
353
|
preIxs.push(
|
|
361
354
|
createAssociatedTokenAccountIdempotentInstruction(
|
|
362
355
|
trader,
|
|
@@ -371,24 +364,6 @@ export class AMMClient {
|
|
|
371
364
|
pool.mintB
|
|
372
365
|
)
|
|
373
366
|
);
|
|
374
|
-
|
|
375
|
-
// Wrap input SOL if needed
|
|
376
|
-
if (inputMint.equals(NATIVE_MINT)) {
|
|
377
|
-
const input = typeof inputAmount === "number" ? new BN(inputAmount) : inputAmount;
|
|
378
|
-
preIxs.push(
|
|
379
|
-
SystemProgram.transfer({
|
|
380
|
-
fromPubkey: trader,
|
|
381
|
-
toPubkey: inputAta,
|
|
382
|
-
lamports: BigInt(input.toString()),
|
|
383
|
-
}),
|
|
384
|
-
createSyncNativeInstruction(inputAta)
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Unwrap output SOL if needed
|
|
389
|
-
if (outputMint.equals(NATIVE_MINT)) {
|
|
390
|
-
postIxs.push(createCloseAccountInstruction(outputAta, trader, trader));
|
|
391
|
-
}
|
|
392
367
|
}
|
|
393
368
|
|
|
394
369
|
let builder = swapIx(
|
|
@@ -408,9 +383,6 @@ export class AMMClient {
|
|
|
408
383
|
if (preIxs.length > 0) {
|
|
409
384
|
builder = builder.preInstructions(preIxs);
|
|
410
385
|
}
|
|
411
|
-
if (postIxs.length > 0) {
|
|
412
|
-
builder = builder.postInstructions(postIxs);
|
|
413
|
-
}
|
|
414
386
|
|
|
415
387
|
return builder;
|
|
416
388
|
}
|
|
@@ -434,8 +406,7 @@ export class AMMClient {
|
|
|
434
406
|
* - Fetches current reserves and computes quote
|
|
435
407
|
* - Calculates minOutput based on slippage tolerance
|
|
436
408
|
* - Adds compute budget instruction (if includeCuBudget is true)
|
|
437
|
-
* - Creates token accounts if they don't exist (if
|
|
438
|
-
* - Handles WSOL wrapping/unwrapping if needed (if autoWrapUnwrap is true)
|
|
409
|
+
* - Creates token accounts if they don't exist (if autoCreateTokenAccounts is true)
|
|
439
410
|
*/
|
|
440
411
|
async swapWithSlippage(
|
|
441
412
|
trader: PublicKey,
|
|
@@ -445,7 +416,7 @@ export class AMMClient {
|
|
|
445
416
|
slippagePercent: number = 0.5,
|
|
446
417
|
options?: AmmActionOptions
|
|
447
418
|
) {
|
|
448
|
-
const {
|
|
419
|
+
const { autoCreateTokenAccounts = true, includeCuBudget = true, computeUnits } = options ?? {};
|
|
449
420
|
|
|
450
421
|
const pool = await this.fetchPool(poolPda);
|
|
451
422
|
const input = typeof inputAmount === "number" ? new BN(inputAmount) : inputAmount;
|
|
@@ -476,7 +447,6 @@ export class AMMClient {
|
|
|
476
447
|
);
|
|
477
448
|
|
|
478
449
|
const preIxs: TransactionInstruction[] = [];
|
|
479
|
-
const postIxs: TransactionInstruction[] = [];
|
|
480
450
|
|
|
481
451
|
if (includeCuBudget) {
|
|
482
452
|
preIxs.push(
|
|
@@ -486,7 +456,7 @@ export class AMMClient {
|
|
|
486
456
|
);
|
|
487
457
|
}
|
|
488
458
|
|
|
489
|
-
if (
|
|
459
|
+
if (autoCreateTokenAccounts) {
|
|
490
460
|
preIxs.push(
|
|
491
461
|
createAssociatedTokenAccountIdempotentInstruction(
|
|
492
462
|
trader,
|
|
@@ -503,35 +473,9 @@ export class AMMClient {
|
|
|
503
473
|
);
|
|
504
474
|
}
|
|
505
475
|
|
|
506
|
-
// Handle WSOL for input/output tokens
|
|
507
|
-
const inputMint = swapAToB ? pool.mintA : pool.mintB;
|
|
508
|
-
const outputMint = swapAToB ? pool.mintB : pool.mintA;
|
|
509
|
-
const inputAta = swapAToB ? traderAccountA : traderAccountB;
|
|
510
|
-
const outputAta = swapAToB ? traderAccountB : traderAccountA;
|
|
511
|
-
|
|
512
|
-
if (autoWrapUnwrap && inputMint.equals(NATIVE_MINT)) {
|
|
513
|
-
// Wrap SOL before swap
|
|
514
|
-
preIxs.push(
|
|
515
|
-
SystemProgram.transfer({
|
|
516
|
-
fromPubkey: trader,
|
|
517
|
-
toPubkey: inputAta,
|
|
518
|
-
lamports: BigInt(input.toString()),
|
|
519
|
-
}),
|
|
520
|
-
createSyncNativeInstruction(inputAta)
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (autoWrapUnwrap && outputMint.equals(NATIVE_MINT)) {
|
|
525
|
-
// Unwrap SOL after swap
|
|
526
|
-
postIxs.push(createCloseAccountInstruction(outputAta, trader, trader));
|
|
527
|
-
}
|
|
528
|
-
|
|
529
476
|
if (preIxs.length > 0) {
|
|
530
477
|
builder = builder.preInstructions(preIxs);
|
|
531
478
|
}
|
|
532
|
-
if (postIxs.length > 0) {
|
|
533
|
-
builder = builder.postInstructions(postIxs);
|
|
534
|
-
}
|
|
535
479
|
|
|
536
480
|
return {
|
|
537
481
|
builder,
|
package/src/amm/types.ts
CHANGED
|
@@ -57,5 +57,6 @@ export type AMMEvent =
|
|
|
57
57
|
/* Options */
|
|
58
58
|
|
|
59
59
|
export interface AmmActionOptions extends TxOptions {
|
|
60
|
-
autoWrapUnwrap?: boolean; // Auto wrap/unwrap native SOL (default: true)
|
|
60
|
+
autoWrapUnwrap?: boolean; // Auto wrap/unwrap native SOL (default: true) - for liquidity operations
|
|
61
|
+
autoCreateTokenAccounts?: boolean; // Auto create token accounts (default: true) - for swaps
|
|
61
62
|
}
|
package/src/futarchy/client.ts
CHANGED
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
TransactionMessage,
|
|
16
16
|
VersionedTransaction,
|
|
17
17
|
} from "@solana/web3.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getAssociatedTokenAddressSync,
|
|
20
|
+
TOKEN_PROGRAM_ID,
|
|
21
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
22
|
+
createAssociatedTokenAccountIdempotentInstruction,
|
|
23
|
+
} from "@solana/spl-token";
|
|
19
24
|
import { PROGRAM_ID, SQUADS_PROGRAM_ID } from "./constants";
|
|
20
25
|
import {
|
|
21
26
|
Futarchy,
|
|
@@ -298,6 +303,14 @@ export class FutarchyClient {
|
|
|
298
303
|
// Slice arrays to numOptions (fixed-size arrays from Rust include empty slots)
|
|
299
304
|
const condBaseMints = vault.condBaseMints.slice(0, numOptions);
|
|
300
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
|
+
}
|
|
301
314
|
const pools = proposal.pools.slice(0, numOptions);
|
|
302
315
|
|
|
303
316
|
// Derive all user conditional token ATAs
|
|
@@ -367,6 +380,69 @@ export class FutarchyClient {
|
|
|
367
380
|
return { builder };
|
|
368
381
|
}
|
|
369
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
|
+
|
|
370
446
|
async finalizeProposal(signer: PublicKey, proposalPda: PublicKey, options?: TxOptions) {
|
|
371
447
|
const proposal = await this.fetchProposal(proposalPda);
|
|
372
448
|
const vault = await this.vault.fetchVault(proposal.vault);
|
|
@@ -455,7 +531,206 @@ export class FutarchyClient {
|
|
|
455
531
|
remainingAccounts
|
|
456
532
|
).preInstructions(this.maybeAddComputeBudget(options));
|
|
457
533
|
|
|
458
|
-
return { builder };
|
|
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
|
+
const CHUNK_SIZE = 20;
|
|
619
|
+
for (let i = 0; i < addresses.length; i += CHUNK_SIZE) {
|
|
620
|
+
const chunk = addresses.slice(i, i + CHUNK_SIZE);
|
|
621
|
+
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
|
622
|
+
payer: creator,
|
|
623
|
+
authority: creator,
|
|
624
|
+
lookupTable: altAddress,
|
|
625
|
+
addresses: chunk,
|
|
626
|
+
});
|
|
627
|
+
const extendTx = new Transaction().add(extendIx);
|
|
628
|
+
await provider.sendAndConfirm(extendTx);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return { altAddress };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Builds a versioned transaction for redeeming liquidity with ALT.
|
|
636
|
+
* Required for proposals with 3+ options to avoid exceeding transaction size limits.
|
|
637
|
+
*
|
|
638
|
+
* @param creator - The user redeeming
|
|
639
|
+
* @param proposalPda - The proposal PDA
|
|
640
|
+
* @param altAddress - Optional ALT address (will be created if not provided for 3+ options)
|
|
641
|
+
* @returns Unsigned versioned transaction, ALT address, and number of options
|
|
642
|
+
*/
|
|
643
|
+
async redeemLiquidityVersioned(
|
|
644
|
+
creator: PublicKey,
|
|
645
|
+
proposalPda: PublicKey,
|
|
646
|
+
altAddress?: PublicKey,
|
|
647
|
+
): Promise<{ versionedTx: VersionedTransaction; altAddress: PublicKey; numOptions: number }> {
|
|
648
|
+
const provider = this.program.provider as AnchorProvider;
|
|
649
|
+
const { builder, numOptions } = await this.redeemLiquidity(creator, proposalPda);
|
|
650
|
+
|
|
651
|
+
// Create ALT if not provided and needed
|
|
652
|
+
let altPubkey = altAddress;
|
|
653
|
+
let verifiedALT: AddressLookupTableAccount | null = null;
|
|
654
|
+
|
|
655
|
+
if (!altPubkey && numOptions >= 3) {
|
|
656
|
+
console.log(` Creating redemption ALT for ${numOptions} options...`);
|
|
657
|
+
const result = await this.createRedemptionALT(creator, proposalPda);
|
|
658
|
+
altPubkey = result.altAddress;
|
|
659
|
+
console.log(` ✓ Redemption ALT created: ${altPubkey.toBase58()}`);
|
|
660
|
+
|
|
661
|
+
// Wait for ALT to be fully available with all addresses
|
|
662
|
+
// Use longer delays after extending to ensure propagation
|
|
663
|
+
console.log(` Waiting for ALT propagation...`);
|
|
664
|
+
const expectedAddresses = 18 + (numOptions * 4); // Base accounts + per-option accounts
|
|
665
|
+
let attempts = 0;
|
|
666
|
+
|
|
667
|
+
// Initial delay after extension
|
|
668
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
669
|
+
|
|
670
|
+
while (attempts < 30) {
|
|
671
|
+
const altAccount = await provider.connection.getAddressLookupTable(altPubkey, {
|
|
672
|
+
commitment: 'confirmed',
|
|
673
|
+
});
|
|
674
|
+
if (altAccount.value) {
|
|
675
|
+
const addressCount = altAccount.value.state.addresses.length;
|
|
676
|
+
console.log(` Attempt ${attempts + 1}: ALT has ${addressCount}/${expectedAddresses} addresses`);
|
|
677
|
+
if (addressCount >= expectedAddresses) {
|
|
678
|
+
verifiedALT = altAccount.value;
|
|
679
|
+
console.log(` ✓ ALT verified with ${addressCount} addresses`);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
684
|
+
attempts++;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!verifiedALT) {
|
|
688
|
+
throw new Error(`ALT failed to populate with expected ${expectedAddresses} addresses after ${attempts} attempts`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!altPubkey) {
|
|
693
|
+
throw new Error("ALT address required for multi-option redemption");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Fetch ALT if we don't have it verified already
|
|
697
|
+
if (!verifiedALT) {
|
|
698
|
+
verifiedALT = await this.fetchALT(altPubkey);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Build instruction
|
|
702
|
+
const instruction = await builder.instruction();
|
|
703
|
+
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 });
|
|
704
|
+
|
|
705
|
+
// Get fresh blockhash
|
|
706
|
+
const { blockhash } = await provider.connection.getLatestBlockhash();
|
|
707
|
+
|
|
708
|
+
// Build versioned transaction using the verified ALT (no re-fetch)
|
|
709
|
+
const versionedTx = this.buildVersionedTxWithALT(
|
|
710
|
+
creator,
|
|
711
|
+
[computeBudgetIx, instruction],
|
|
712
|
+
verifiedALT,
|
|
713
|
+
blockhash,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Return the versioned transaction for the caller to sign and send
|
|
717
|
+
// This allows the caller to use their own signing mechanism (e.g., keypair.sign)
|
|
718
|
+
return { versionedTx, altAddress: altPubkey, numOptions };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Helper to send a signed versioned transaction.
|
|
723
|
+
*/
|
|
724
|
+
async sendVersionedTransaction(
|
|
725
|
+
signedTx: VersionedTransaction,
|
|
726
|
+
): Promise<string> {
|
|
727
|
+
const provider = this.program.provider as AnchorProvider;
|
|
728
|
+
const signature = await provider.connection.sendTransaction(signedTx, {
|
|
729
|
+
skipPreflight: false,
|
|
730
|
+
preflightCommitment: 'confirmed',
|
|
731
|
+
});
|
|
732
|
+
await provider.connection.confirmTransaction(signature, 'confirmed');
|
|
733
|
+
return signature;
|
|
459
734
|
}
|
|
460
735
|
|
|
461
736
|
/* Address Lookup Table */
|
|
@@ -566,12 +841,21 @@ export class FutarchyClient {
|
|
|
566
841
|
instructions: TransactionInstruction[],
|
|
567
842
|
altAddress: PublicKey,
|
|
568
843
|
): Promise<VersionedTransaction> {
|
|
844
|
+
const provider = this.program.provider as AnchorProvider;
|
|
569
845
|
const alt = await this.fetchALT(altAddress);
|
|
570
|
-
const blockhash = await
|
|
846
|
+
const { blockhash } = await provider.connection.getLatestBlockhash();
|
|
847
|
+
return this.buildVersionedTxWithALT(payer, instructions, alt, blockhash);
|
|
848
|
+
}
|
|
571
849
|
|
|
850
|
+
buildVersionedTxWithALT(
|
|
851
|
+
payer: PublicKey,
|
|
852
|
+
instructions: TransactionInstruction[],
|
|
853
|
+
alt: AddressLookupTableAccount,
|
|
854
|
+
blockhash: string,
|
|
855
|
+
): VersionedTransaction {
|
|
572
856
|
const message = new TransactionMessage({
|
|
573
857
|
payerKey: payer,
|
|
574
|
-
recentBlockhash: blockhash
|
|
858
|
+
recentBlockhash: blockhash,
|
|
575
859
|
instructions,
|
|
576
860
|
}).compileToV0Message([alt]);
|
|
577
861
|
|
|
@@ -729,57 +1013,4 @@ export class FutarchyClient {
|
|
|
729
1013
|
|
|
730
1014
|
return { builder, daoPda, moderatorPda };
|
|
731
1015
|
}
|
|
732
|
-
|
|
733
|
-
/* High-Level Proposal Creator */
|
|
734
|
-
|
|
735
|
-
async createProposal(
|
|
736
|
-
creator: PublicKey,
|
|
737
|
-
moderatorPda: PublicKey,
|
|
738
|
-
proposalParams: ProposalParams,
|
|
739
|
-
baseAmount: BN | number,
|
|
740
|
-
quoteAmount: BN | number,
|
|
741
|
-
numOptions: number = 2,
|
|
742
|
-
metadata?: string,
|
|
743
|
-
options?: TxOptions
|
|
744
|
-
) {
|
|
745
|
-
if (numOptions < 2) {
|
|
746
|
-
throw new Error("Minimum 2 options required");
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 1. Create ALT (sends txs internally)
|
|
750
|
-
const { altAddress } = await this.createProposalALT(creator, moderatorPda, numOptions);
|
|
751
|
-
|
|
752
|
-
// 2. Build initializeProposal
|
|
753
|
-
const initResult = await this.initializeProposal(creator, moderatorPda, proposalParams, metadata, options);
|
|
754
|
-
|
|
755
|
-
// 3. Build addOption for each additional option (beyond initial 2)
|
|
756
|
-
const addOptionBuilders: Awaited<ReturnType<typeof this.addOption>>["builder"][] = [];
|
|
757
|
-
const additionalPools: PublicKey[] = [];
|
|
758
|
-
const additionalCondBaseMints: PublicKey[] = [];
|
|
759
|
-
const additionalCondQuoteMints: PublicKey[] = [];
|
|
760
|
-
|
|
761
|
-
for (let i = 2; i < numOptions; i++) {
|
|
762
|
-
const addResult = await this.addOption(creator, initResult.proposalPda, options);
|
|
763
|
-
addOptionBuilders.push(addResult.builder);
|
|
764
|
-
additionalPools.push(addResult.pool);
|
|
765
|
-
additionalCondBaseMints.push(addResult.condBaseMint);
|
|
766
|
-
additionalCondQuoteMints.push(addResult.condQuoteMint);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// 4. Build launchProposal
|
|
770
|
-
const launchResult = await this.launchProposal(creator, initResult.proposalPda, baseAmount, quoteAmount, options);
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
altAddress,
|
|
774
|
-
proposalPda: initResult.proposalPda,
|
|
775
|
-
proposalId: initResult.proposalId,
|
|
776
|
-
vaultPda: initResult.vaultPda,
|
|
777
|
-
pools: [...initResult.pools, ...additionalPools],
|
|
778
|
-
condBaseMints: [...initResult.condBaseMints, ...additionalCondBaseMints],
|
|
779
|
-
condQuoteMints: [...initResult.condQuoteMints, ...additionalCondQuoteMints],
|
|
780
|
-
initializeBuilder: initResult.builder,
|
|
781
|
-
addOptionBuilders,
|
|
782
|
-
launchBuilder: launchResult.builder,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
1016
|
}
|
package/src/utils.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
export interface TxOptions {
|
|
9
9
|
includeCuBudget?: boolean; // Include compute budget instruction (default: true)
|
|
10
10
|
computeUnits?: number; // Override default compute units
|
|
11
|
+
/**
|
|
12
|
+
* Pre-create conditional token ATAs before launch to avoid exceeding
|
|
13
|
+
* Solana's 64 instruction trace limit. Default: true for 3+ options.
|
|
14
|
+
* Set to false if you've already created ATAs or want to manage them manually.
|
|
15
|
+
*/
|
|
16
|
+
ensureATAs?: boolean;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/*
|