1+ import { confirm } from '@inquirer/prompts' ;
12import {
3+ ComputeBudgetProgram ,
24 PublicKey ,
35 Transaction ,
46 TransactionInstruction ,
@@ -20,15 +22,16 @@ import { chainsToSkip } from '../../src/config/chain.js';
2022import { DeployEnvironment } from '../../src/config/environment.js' ;
2123import { squadsConfigs } from '../../src/config/squads.js' ;
2224import {
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' ;
3235import { getTurnkeySealevelDeployerSigner } from '../../src/utils/turnkey.js' ;
3336import { chainIsProtocol , readJSONAtPath } from '../../src/utils/utils.js' ;
3437import { 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