Skip to content

Commit 5402ba5

Browse files
authored
feat(infra): svm ism updates via turnkey (#7319)
1 parent f7a5ef0 commit 5402ba5

File tree

7 files changed

+957
-113
lines changed

7 files changed

+957
-113
lines changed

typescript/infra/scripts/sealevel-helpers/update-multisig-ism-config.ts

Lines changed: 108 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { confirm } from '@inquirer/prompts';
12
import {
3+
ComputeBudgetProgram,
24
PublicKey,
35
Transaction,
46
TransactionInstruction,
@@ -20,15 +22,16 @@ import { chainsToSkip } from '../../src/config/chain.js';
2022
import { DeployEnvironment } from '../../src/config/environment.js';
2123
import { squadsConfigs } from '../../src/config/squads.js';
2224
import {
23-
FIRST_REAL_INSTRUCTION_INDEX,
2425
SvmMultisigConfigMap,
2526
buildMultisigIsmInstructions,
2627
diffMultisigIsmConfigs,
2728
fetchMultisigIsmState,
29+
isComputeBudgetInstruction,
2830
loadCoreProgramIds,
2931
multisigIsmConfigPath,
3032
serializeMultisigIsmDifference,
3133
} from '../../src/utils/sealevel.js';
34+
import { submitProposalToSquads } from '../../src/utils/squads.js';
3235
import { getTurnkeySealevelDeployerSigner } from '../../src/utils/turnkey.js';
3336
import { chainIsProtocol, readJSONAtPath } from '../../src/utils/utils.js';
3437
import { getArgs, withChains } from '../agent-utils.js';
@@ -104,14 +107,15 @@ function analyzeConfigDifferences(
104107
}
105108

106109
/**
107-
* Log MultisigIsm update transaction for manual Squads proposal
110+
* Log MultisigIsm update transaction and optionally submit to Squads
108111
*/
109-
async function logMultisigIsmUpdateTransaction(
112+
async function logAndSubmitMultisigIsmUpdateTransaction(
110113
chain: ChainName,
111114
instructions: TransactionInstruction[],
112115
owner: PublicKey,
113116
configsToUpdate: SvmMultisigConfigMap,
114117
mpp: MultiProtocolProvider,
118+
signerAdapter: SvmMultiProtocolSignerAdapter,
115119
): Promise<void> {
116120
rootLogger.info(chalk.cyan('\n=== Batched Transaction ==='));
117121
rootLogger.info(chalk.gray(`Total instructions: ${instructions.length}`));
@@ -124,26 +128,56 @@ async function logMultisigIsmUpdateTransaction(
124128
// Sort chain names alphabetically (same order as buildMultisigIsmInstructions)
125129
const sortedChainNames = Object.keys(configsToUpdate).sort();
126130

131+
// Dynamically detect which instructions are compute budget vs MultisigIsm
132+
// This handles different chains potentially having different setup instructions
133+
let multisigInstructionIndex = 0;
134+
127135
// Log each instruction summary with data hex for verification
128136
instructions.forEach((instruction, idx) => {
129-
if (idx === 0) {
130-
rootLogger.info(
131-
chalk.gray(`\nInstruction ${idx}: Set compute unit limit to 1,400,000`),
137+
const isComputeBudget = isComputeBudgetInstruction(instruction);
138+
139+
if (isComputeBudget) {
140+
// Decode compute budget instruction type
141+
const dataView = new DataView(
142+
instruction.data.buffer,
143+
instruction.data.byteOffset,
144+
instruction.data.byteLength,
132145
);
146+
const instructionType = dataView.getUint8(0);
147+
148+
if (instructionType === 1) {
149+
rootLogger.info(
150+
chalk.gray(`Instruction ${idx}: Request heap frame (compute budget)`),
151+
);
152+
} else if (instructionType === 2) {
153+
rootLogger.info(
154+
chalk.gray(
155+
`Instruction ${idx}: Set compute unit limit (compute budget)`,
156+
),
157+
);
158+
} else {
159+
rootLogger.info(
160+
chalk.gray(
161+
`Instruction ${idx}: Compute budget (type ${instructionType})`,
162+
),
163+
);
164+
}
133165
} else {
134-
const chainIndex = idx - FIRST_REAL_INSTRUCTION_INDEX;
135-
const remoteChain = sortedChainNames[chainIndex];
166+
// MultisigIsm instruction
167+
const remoteChain = sortedChainNames[multisigInstructionIndex];
136168
const config = configsToUpdate[remoteChain];
137169
rootLogger.info(
138170
chalk.gray(
139171
`Instruction ${idx}: Set validators and threshold for ${remoteChain} (${config.validators.length} validators, threshold ${config.threshold})`,
140172
),
141173
);
142-
}
143174

144-
// Debug log instruction data
145-
const dataHex = instruction.data.toString('hex');
146-
rootLogger.debug(chalk.gray(` Data: ${dataHex}`));
175+
// Debug log instruction data
176+
const dataHex = instruction.data.toString('hex');
177+
rootLogger.debug(chalk.gray(` Data: ${dataHex}`));
178+
179+
multisigInstructionIndex++;
180+
}
147181
});
148182

149183
// Create a transaction with ALL instructions
@@ -159,62 +193,101 @@ async function logMultisigIsmUpdateTransaction(
159193

160194
instructions.forEach((ix) => transaction.add(ix));
161195

196+
const isSolana = chain === 'solanamainnet';
197+
162198
// Serialize transaction to base58 (for Solana Squads)
163199
const txBase58 = bs58.encode(
164200
new Uint8Array(transaction.serialize({ requireAllSignatures: false })),
165201
);
166-
rootLogger.info(
167-
chalk.green(`\nTransaction (base58) - for Solana Squads:\n${txBase58}`),
168-
);
169202

170203
// Serialize message to base58 (for alt SVM Squads UIs like Eclipse)
171204
const message = transaction.compileMessage();
172205
const messageBase58 = bs58.encode(new Uint8Array(message.serialize()));
173-
rootLogger.info(
174-
chalk.magenta(
175-
`\nMessage (base58) - for alt SVM Squads UIs:\n${messageBase58}\n`,
176-
),
177-
);
206+
207+
if (isSolana) {
208+
rootLogger.info(
209+
chalk.green(`\nTransaction (base58) - for Solana Squads:\n${txBase58}`),
210+
);
211+
} else {
212+
rootLogger.info(
213+
chalk.magenta(
214+
`\nMessage (base58) - for alt SVM Squads UIs:\n${messageBase58}\n`,
215+
),
216+
);
217+
}
218+
219+
// Create descriptive memo for the proposal
220+
const chainNames = sortedChainNames.join(', ');
221+
const updateCount = sortedChainNames.length;
222+
const memo = `Update MultisigIsm validators for ${updateCount} chain${updateCount > 1 ? 's' : ''}: ${chainNames}`;
223+
224+
// Prompt for Squads submission
225+
const shouldSubmitToSquads = await confirm({
226+
message:
227+
'Do you want to submit this proposal to Squads multisig automatically?',
228+
default: true,
229+
});
230+
231+
if (!shouldSubmitToSquads) {
232+
rootLogger.info(
233+
chalk.yellow(
234+
`\nSkipping Squads submission. Use the base58 ${isSolana ? 'transaction' : 'message'} above to submit manually.`,
235+
),
236+
);
237+
return;
238+
}
239+
240+
// Submit to Squads
241+
await submitProposalToSquads(chain, instructions, mpp, signerAdapter, memo);
178242
} catch (error) {
179-
rootLogger.warn(
180-
chalk.yellow(`Could not serialize transaction/message: ${error}`),
181-
);
243+
rootLogger.error(chalk.red(`Failed to log/submit transaction: ${error}`));
244+
throw error;
182245
}
183246
}
184247

185248
/**
186249
* Print update instructions as a single batched transaction for Squads multisig submission
187250
*/
188-
async function printMultisigIsmUpdates(
251+
async function printAndSubmitMultisigIsmUpdates(
189252
chain: ChainName,
190253
multisigIsmProgramId: PublicKey,
191254
vaultPubkey: PublicKey,
192255
configsToUpdate: SvmMultisigConfigMap,
193256
mpp: MultiProtocolProvider,
257+
signerAdapter: SvmMultiProtocolSignerAdapter,
194258
): Promise<number> {
195259
if (Object.keys(configsToUpdate).length === 0) {
196260
return 0;
197261
}
198262

199263
const instructions = buildMultisigIsmInstructions(
264+
chain,
200265
multisigIsmProgramId,
201266
vaultPubkey,
202267
configsToUpdate,
203268
mpp,
204269
);
205270

271+
// Count instruction types dynamically
272+
const computeBudgetCount = instructions.filter(
273+
isComputeBudgetInstruction,
274+
).length;
206275
const updateCount = Object.keys(configsToUpdate).length;
276+
277+
const budgetNote =
278+
computeBudgetCount === 0 ? ' (compute budget handled by Squads UI)' : '';
207279
rootLogger.debug(
208-
`[${chain}] Generating batched transaction with ${instructions.length} instructions (${updateCount} MultisigIsm updates + 1 compute budget)`,
280+
`[${chain}] Generating batched transaction with ${instructions.length} instructions (${computeBudgetCount} compute budget + ${updateCount} MultisigIsm updates)${budgetNote}`,
209281
);
210282

211-
// Log the batched transaction for manual Squads multisig proposal submission
212-
await logMultisigIsmUpdateTransaction(
283+
// Log the batched transaction and optionally submit to Squads
284+
await logAndSubmitMultisigIsmUpdateTransaction(
213285
chain,
214286
instructions,
215287
vaultPubkey,
216288
configsToUpdate,
217289
mpp,
290+
signerAdapter,
218291
);
219292

220293
return updateCount;
@@ -253,7 +326,7 @@ async function processChain(
253326
const vaultPubkey = new PublicKey(squadsConfigs[chain].vault);
254327
rootLogger.debug(`Using Squads vault (multisig): ${vaultPubkey.toBase58()}`);
255328
rootLogger.debug(
256-
`Using Turnkey signer (owner/approver): ${await adapter.address()}`,
329+
`Using Turnkey signer (proposal creator): ${await adapter.address()}`,
257330
);
258331

259332
// Load configuration from file
@@ -288,12 +361,13 @@ async function processChain(
288361
`Found ${Object.keys(configsToUpdate).length} MultisigIsm configs to update for ${chain}`,
289362
);
290363

291-
updated = await printMultisigIsmUpdates(
364+
updated = await printAndSubmitMultisigIsmUpdates(
292365
chain,
293366
multisigIsmProgramId,
294367
vaultPubkey,
295368
configsToUpdate,
296369
mpp,
370+
adapter,
297371
);
298372
} else {
299373
rootLogger.info(`No updates needed for ${chain} - all configs match`);
@@ -308,18 +382,10 @@ async function main() {
308382
environment,
309383
chains: chainsArg,
310384
context = Contexts.Hyperlane,
311-
apply,
312385
} = await withChains(getArgs())
313386
.describe('context', 'MultisigIsm context to update')
314387
.choices('context', [Contexts.Hyperlane, Contexts.ReleaseCandidate])
315-
.alias('x', 'context')
316-
.option('apply', {
317-
type: 'boolean',
318-
description: 'Apply changes on-chain (default is dry-run mode)',
319-
default: false,
320-
}).argv;
321-
322-
const dryRun = !apply;
388+
.alias('x', 'context').argv;
323389

324390
// Compute default chains based on environment
325391
const envConfig = getEnvironmentConfig(environment);
@@ -335,27 +401,15 @@ async function main() {
335401
// Initialize Turnkey signer
336402
rootLogger.info('Initializing Turnkey signer from GCP Secret Manager...');
337403
const turnkeySigner = await getTurnkeySealevelDeployerSigner(environment);
338-
rootLogger.info(`Signer public key: ${turnkeySigner.publicKey.toBase58()}`);
404+
const creatorPublicKey = turnkeySigner.publicKey;
405+
rootLogger.info(
406+
`Proposal creator public key: ${creatorPublicKey.toBase58()}`,
407+
);
339408

340409
rootLogger.info(
341410
`Configuring MultisigIsm for chains: ${chains.join(', ')} on ${environment} (context: ${context})`,
342411
);
343412

344-
if (!dryRun) {
345-
rootLogger.warn(
346-
chalk.yellow(
347-
'⚠️ WARNING: --apply flag is not yet implemented. Running in dry-run mode.',
348-
),
349-
);
350-
rootLogger.warn(
351-
chalk.yellow(
352-
' Transactions will be printed for manual Squads multisig submission.',
353-
),
354-
);
355-
} else {
356-
rootLogger.info('Running in DRY RUN mode - no transactions will be sent');
357-
}
358-
359413
// Process all chains sequentially (to avoid overwhelming the user with prompts)
360414
const mpp = await envConfig.getMultiProtocolProvider();
361415
const results: Array<{ chain: string; updated: number; matched: number }> =

0 commit comments

Comments
 (0)