Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions scripts/utils/proposals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as anchor from "@coral-xyz/anchor";
import { InstructionUtils } from "@metadaoproject/programs";
import {
FutarchyClient,
getProposalAddr,
} from "@metadaoproject/programs/futarchy";
import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js";
import { sha256 } from "@noble/hashes/sha256";

const provider = anchor.AnchorProvider.env();

const futarchyClient = FutarchyClient.createClient({ provider });

export const initializeFutarchyProposal = async (
daoAddress: PublicKey,
squadsProposalPda: PublicKey,
) => {
const dao = await futarchyClient.getDao(daoAddress);
const [proposal] = getProposalAddr(
futarchyClient.futarchy.programId,
squadsProposalPda,
);

const existing = await futarchyClient.fetchProposal(proposal);
if (existing) {
console.log(" Futarchy Proposal PDA:", proposal.toBase58());
console.log(" ✓ Futarchy proposal already initialized");
return proposal;
}

const { question, baseVault, quoteVault } = futarchyClient.getProposalPdas(
proposal,
dao.baseMint,
dao.quoteMint,
daoAddress,
);

const questionAccount =
await futarchyClient.vaultClient.fetchQuestion(question);
if (!questionAccount) {
await futarchyClient.vaultClient.initializeQuestion(
sha256(`Will ${proposal} pass?/FAIL/PASS`),
proposal,
2,
);
console.log(" ✓ Question initialized");
} else {
console.log(" ✓ Question already exists");
}

const [baseVaultAccount, quoteVaultAccount] = await Promise.all([
futarchyClient.vaultClient.fetchVault(baseVault),
futarchyClient.vaultClient.fetchVault(quoteVault),
]);

if (!baseVaultAccount || !quoteVaultAccount) {
// Avoid starting vault init if only one side exists — that would fail mid-way.
if (baseVaultAccount || quoteVaultAccount) {
throw new Error(
`Partial vault state: base=${!!baseVaultAccount} quote=${!!quoteVaultAccount}`,
);
}
await futarchyClient.vaultClient
.initializeVaultIx(question, dao.baseMint, 2)
.postInstructions(
await InstructionUtils.getInstructions(
futarchyClient.vaultClient.initializeVaultIx(
question,
dao.quoteMint,
2,
),
),
)
.rpc();
console.log(" ✓ Vaults initialized");
} else {
console.log(" ✓ Vaults already exist");
}

// initialize_proposal itself does not need the conditional ATAs; the SDK
// still creates them as preInstructions for later AMM use. Those CreateIdempotent
// calls require the conditional mints to already exist (vaults above).
await futarchyClient
.initializeProposalIx(
squadsProposalPda,
daoAddress,
dao.baseMint,
dao.quoteMint,
question,
)
.preInstructions([
ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
])
.rpc();

console.log(" Futarchy Proposal PDA:", proposal.toBase58());
console.log(" ✓ Futarchy proposal initialized");

return proposal;
};
13 changes: 13 additions & 0 deletions scripts/utils/squads.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PERMISSIONLESS_ACCOUNT } from "@metadaoproject/programs";
import { PublicKey, TransactionMessage } from "@solana/web3.js";
import * as multisig from "@sqds/multisig";
import * as anchor from "@coral-xyz/anchor";

// Returns the multisig, spending limit and 0th vault pda for a given dao address
export const getSquadsPdasFromDao = async (
Expand Down Expand Up @@ -33,6 +34,18 @@ export const getSquadsPdasFromDao = async (
};
};

export const getSquadsTxIndex = async (
squadsMultisig: PublicKey,
provider: anchor.AnchorProvider,
) => {
const multisigAccountInfo =
await multisig.accounts.Multisig.fromAccountAddress(
provider.connection, // TODO: Review if we want to instead offer connection class...
squadsMultisig,
);
return Number(multisigAccountInfo.transactionIndex);
};

export const createSquadsVaultTxAndProposal = async (
squadsMultisig: PublicKey,
transactionIndex: bigint,
Expand Down
195 changes: 141 additions & 54 deletions scripts/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { BN } from "bn.js";
import {
AddressLookupTableAccount,
AddressLookupTableProgram,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
Transaction,
TransactionExpiredBlockheightExceededError,
TransactionExpiredTimeoutError,
TransactionInstruction,
} from "@solana/web3.js";

export const TEN_SECONDS_IN_SLOTS = 25n;
Expand All @@ -16,6 +20,74 @@ export const DAY_IN_SLOTS = HOUR_IN_SLOTS * 24n;
export const toBN = (val: bigint): typeof BN.prototype =>
new BN(val.toString());

/** Priority fee for lookup-table transactions (matches the rest of the tooling). */
const LUT_PRIORITY_FEE_MICRO_LAMPORTS = parseInt(
process.env.PRIORITY_FEE_MICRO_LAMPORTS ?? "10000",
10,
);

/** Attempts per lookup-table transaction before giving up. */
const LUT_TX_RETRIES = 3;

/**
* Send one lookup-table transaction and wait for confirmation, throwing on an
* on-chain error. Confirmation matters here: extends fail if the create
* hasn't landed, and a silent drop would otherwise only surface as a generic
* timeout in the finalization check.
*
* Expired (dropped) transactions are retried with a fresh blockhash. That is
* safe for extends — a false expiry only appends duplicate addresses, which
* compileToV0Message handles fine and the finalization check ignores.
*/
async function sendAndConfirmLutTx(
connection: Connection,
instruction: TransactionInstruction,
payer: Keypair,
authority: Keypair,
label: string,
): Promise<void> {
for (let attempt = 1; attempt <= LUT_TX_RETRIES; attempt++) {
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();

const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: LUT_PRIORITY_FEE_MICRO_LAMPORTS,
}),
instruction,
);
tx.recentBlockhash = blockhash;
tx.feePayer = payer.publicKey;
tx.sign(payer, authority);

const signature = await connection.sendRawTransaction(tx.serialize());
try {
const confirmation = await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
"confirmed",
);
if (confirmation.value.err) {
throw new Error(
`${label} failed on-chain: ${JSON.stringify(confirmation.value.err)} (tx ${signature})`,
);
}
console.log(`${label} confirmed: ${signature}`);
return;
} catch (err) {
const isExpiry =
err instanceof TransactionExpiredBlockheightExceededError ||
err instanceof TransactionExpiredTimeoutError;
if (isExpiry && attempt < LUT_TX_RETRIES) {
console.log(
`${label} expired — retrying with a fresh blockhash (attempt ${attempt}/${LUT_TX_RETRIES})`,
);
continue;
}
throw err;
}
}
}

/**
* Creates a lookup table for all unique accounts in a transaction
* @param transaction - The transaction to create a lookup table for
Expand All @@ -32,14 +104,6 @@ export async function createLookupTableForTransaction(
// use a different authority for the lookup table to avoid conflicts
const lookupAuthority = Keypair.generate();
// Should ^ be the payer so we can update it?
const slot = await connection.getSlot();

const [createTableIx, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: lookupAuthority.publicKey,
payer: payer.publicKey,
recentSlot: slot - 1,
});

// Extract all unique accounts from the transaction
const accountsToAdd = transaction.instructions.map((instruction) =>
Expand All @@ -52,21 +116,42 @@ export async function createLookupTableForTransaction(
const allAddresses = [...uniqueAccounts, ...additionalAddresses];
const finalUniqueAddresses = [...new Set(allAddresses)] as PublicKey[];

// Create the lookup table
let createLutTx = new Transaction().add(createTableIx);
let blockhash = await connection.getLatestBlockhash();

createLutTx.recentBlockhash = blockhash.blockhash;
createLutTx.feePayer = payer.publicKey;
createLutTx.sign(payer, lookupAuthority);

await connection.sendRawTransaction(createLutTx.serialize(), {
skipPreflight: true,
});
await new Promise((resolve) => setTimeout(resolve, 2000));
// Create the lookup table — must be confirmed before any extend can land.
// Retried with a re-derived address: createLookupTable requires a slot that
// is still in the SlotHashes sysvar, so a create that failed (e.g. a stale
// getSlot from a lagging RPC) needs a fresh slot, which changes the derived
// table address. An abandoned attempt just strands dust rent.
let lookupTableAddress: PublicKey | undefined;
for (let attempt = 1; lookupTableAddress === undefined; attempt++) {
const slot = await connection.getSlot();
const [createTableIx, tableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: lookupAuthority.publicKey,
payer: payer.publicKey,
recentSlot: slot - 1,
});
try {
await sendAndConfirmLutTx(
connection,
createTableIx,
payer,
lookupAuthority,
"Create lookup table",
);
lookupTableAddress = tableAddress;
} catch (err) {
if (attempt >= LUT_TX_RETRIES) throw err;
console.log(
`Create lookup table failed (${err instanceof Error ? err.message : String(err)}) — retrying with a fresh slot`,
);
}
}

// Extend the lookup table with all unique accounts
const addressesPerExtend = 20;
const totalExtends = Math.ceil(
finalUniqueAddresses.length / addressesPerExtend,
);
for (let i = 0; i < finalUniqueAddresses.length; i += addressesPerExtend) {
const batch = finalUniqueAddresses.slice(i, i + addressesPerExtend);

Expand All @@ -77,42 +162,44 @@ export async function createLookupTableForTransaction(
addresses: batch,
});

let extendLutTx = new Transaction().add(extendTableIx);
blockhash = await connection.getLatestBlockhash();
extendLutTx.recentBlockhash = blockhash.blockhash;
extendLutTx.feePayer = payer.publicKey;
extendLutTx.sign(payer, lookupAuthority);
await sendAndConfirmLutTx(
connection,
extendTableIx,
payer,
lookupAuthority,
`Extend lookup table ${Math.floor(i / addressesPerExtend) + 1}/${totalExtends}`,
);
}

await connection.sendRawTransaction(extendLutTx.serialize(), {
skipPreflight: true,
});
// Wait until the FINALIZED view of the table contains every address. That
// guarantees both halves of usability: no create/extend was dropped (the
// table really holds everything we submitted), and any blockhash fetched
// from here on belongs to a slot after the table's lastExtendedSlot, so a
// v0 transaction referencing it is immediately valid.
const expected = finalUniqueAddresses.map((address) => address.toBase58());
const maxAttempts = 45;
let missing: string[] = expected;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const lookupTableAccount = await connection.getAddressLookupTable(
lookupTableAddress,
{ commitment: "finalized" },
);
const present = new Set(
(lookupTableAccount.value?.state.addresses ?? []).map((address) =>
address.toBase58(),
),
);
missing = expected.filter((address) => !present.has(address));
if (lookupTableAccount.value && missing.length === 0) {
console.log(
`Lookup table ${lookupTableAddress.toBase58()} finalized with ${present.size} addresses`,
);
return lookupTableAccount.value;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}

// Add a dummy account to ensure the lookup table has enough entries for all indexes
const dummyAccount = Keypair.generate().publicKey;
const extendTableIx = AddressLookupTableProgram.extendLookupTable({
authority: lookupAuthority.publicKey,
payer: payer.publicKey,
lookupTable: lookupTableAddress,
addresses: [dummyAccount],
});

let extendLutTx = new Transaction().add(extendTableIx);
blockhash = await connection.getLatestBlockhash();
extendLutTx.recentBlockhash = blockhash.blockhash;
extendLutTx.feePayer = payer.publicKey;
extendLutTx.sign(payer, lookupAuthority);

await connection.sendRawTransaction(extendLutTx.serialize(), {
skipPreflight: true,
});
await new Promise((resolve) => setTimeout(resolve, 2000));

// Fetch and return the lookup table account
let lookupTableAccount =
await connection.getAddressLookupTable(lookupTableAddress);
console.log("created lookupTableAccount", lookupTableAccount);

return lookupTableAccount.value;
throw new Error(
`Lookup table ${lookupTableAddress.toBase58()} did not finalize with all addresses after ${maxAttempts} attempts (${missing.length}/${expected.length} still missing)`,
);
}
8 changes: 8 additions & 0 deletions scripts/v0.7/credible/allocation/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Solana RPC. Staging surfpool, or http://127.0.0.1:8899 for a local surfpool.
RPC_URL=

# Indexer Postgres (read replica) for the boost's fund events. Read-only.
FUTARCHY_PG_URL=

# Launch authority key (base58 or JSON byte array). Only needed for --execute.
CREDIBLE_AUTHORITY_KEY=
6 changes: 6 additions & 0 deletions scripts/v0.7/credible/allocation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.env
.surfpool-run/
allocation.out.json
laso-dryrun.out
allocation.csv
Loading
Loading