11import { BaseCoin as CoinConfig } from '@bitgo/statics' ;
22import { BuildTransactionError , SolInstruction , SolVersionedInstruction , TransactionType } from '@bitgo/sdk-core' ;
3- import { PublicKey } from '@solana/web3.js' ;
3+ import { PublicKey , SystemProgram , SYSVAR_RECENT_BLOCKHASHES_PUBKEY } from '@solana/web3.js' ;
44import { Transaction } from './transaction' ;
55import { TransactionBuilder } from './transactionBuilder' ;
66import { InstructionBuilderTypes } from './constants' ;
@@ -103,17 +103,44 @@ export class CustomInstructionBuilder extends TransactionBuilder {
103103 throw new BuildTransactionError ( 'staticAccountKeys must be a non-empty array' ) ;
104104 }
105105
106- this . addCustomInstructions ( data . versionedInstructions ) ;
106+ if ( ! data . messageHeader || typeof data . messageHeader !== 'object' ) {
107+ throw new BuildTransactionError ( 'messageHeader must be a valid object' ) ;
108+ }
109+ if (
110+ typeof data . messageHeader . numRequiredSignatures !== 'number' ||
111+ data . messageHeader . numRequiredSignatures < 0
112+ ) {
113+ throw new BuildTransactionError ( 'messageHeader.numRequiredSignatures must be a non-negative number' ) ;
114+ }
115+ if (
116+ typeof data . messageHeader . numReadonlySignedAccounts !== 'number' ||
117+ data . messageHeader . numReadonlySignedAccounts < 0
118+ ) {
119+ throw new BuildTransactionError ( 'messageHeader.numReadonlySignedAccounts must be a non-negative number' ) ;
120+ }
121+ if (
122+ typeof data . messageHeader . numReadonlyUnsignedAccounts !== 'number' ||
123+ data . messageHeader . numReadonlyUnsignedAccounts < 0
124+ ) {
125+ throw new BuildTransactionError ( 'messageHeader.numReadonlyUnsignedAccounts must be a non-negative number' ) ;
126+ }
127+
128+ let processedData = data ;
129+ if ( this . _nonceInfo && this . _nonceInfo . params ) {
130+ processedData = this . injectNonceAdvanceInstruction ( data ) ;
131+ }
132+
133+ this . addCustomInstructions ( processedData . versionedInstructions ) ;
107134
108135 if ( ! this . _transaction ) {
109136 this . _transaction = new Transaction ( this . _coinConfig ) ;
110137 }
111- this . _transaction . setVersionedTransactionData ( data ) ;
138+ this . _transaction . setVersionedTransactionData ( processedData ) ;
112139
113140 this . _transaction . setTransactionType ( TransactionType . CustomTx ) ;
114141
115- if ( ! this . _sender && data . staticAccountKeys . length > 0 ) {
116- this . _sender = data . staticAccountKeys [ 0 ] ;
142+ if ( ! this . _sender && processedData . staticAccountKeys . length > 0 ) {
143+ this . _sender = processedData . staticAccountKeys [ 0 ] ;
117144 }
118145
119146 return this ;
@@ -125,6 +152,63 @@ export class CustomInstructionBuilder extends TransactionBuilder {
125152 }
126153 }
127154
155+ /**
156+ * Inject nonce advance instruction into versioned transaction data for durable nonce support.
157+ * Reorders accounts so signers appear first (required by Solana MessageV0 format).
158+ * @param data - Original versioned transaction data
159+ * @returns Modified versioned transaction data with nonce advance instruction
160+ * @private
161+ */
162+ private injectNonceAdvanceInstruction ( data : VersionedTransactionData ) : VersionedTransactionData {
163+ const { walletNonceAddress, authWalletAddress } = this . _nonceInfo ! . params ;
164+ const SYSTEM_PROGRAM = SystemProgram . programId . toBase58 ( ) ;
165+ const SYSVAR_RECENT_BLOCKHASHES = SYSVAR_RECENT_BLOCKHASHES_PUBKEY . toBase58 ( ) ;
166+
167+ const numSigners = data . messageHeader . numRequiredSignatures ;
168+ const originalSigners = data . staticAccountKeys . slice ( 0 , numSigners ) ;
169+ const originalNonSigners = data . staticAccountKeys . slice ( numSigners ) ;
170+
171+ if ( ! originalSigners . includes ( authWalletAddress ) ) {
172+ originalSigners . push ( authWalletAddress ) ;
173+ }
174+
175+ const nonSigners = [ ...originalNonSigners ] ;
176+ const allKeys = [ ...originalSigners , ...originalNonSigners ] ;
177+
178+ if ( ! allKeys . includes ( SYSTEM_PROGRAM ) ) nonSigners . push ( SYSTEM_PROGRAM ) ;
179+ if ( ! allKeys . includes ( walletNonceAddress ) ) nonSigners . push ( walletNonceAddress ) ;
180+ if ( ! allKeys . includes ( SYSVAR_RECENT_BLOCKHASHES ) ) nonSigners . push ( SYSVAR_RECENT_BLOCKHASHES ) ;
181+
182+ const newStaticAccountKeys = [ ...originalSigners , ...nonSigners ] ;
183+
184+ const nonceAdvanceInstruction : SolVersionedInstruction = {
185+ programIdIndex : newStaticAccountKeys . indexOf ( SYSTEM_PROGRAM ) ,
186+ accountKeyIndexes : [
187+ newStaticAccountKeys . indexOf ( walletNonceAddress ) ,
188+ newStaticAccountKeys . indexOf ( SYSVAR_RECENT_BLOCKHASHES ) ,
189+ newStaticAccountKeys . indexOf ( authWalletAddress ) ,
190+ ] ,
191+ data : '6vx8P' , // SystemProgram AdvanceNonceAccount (0x04000000) in base58
192+ } ;
193+
194+ const indexMap = new Map ( data . staticAccountKeys . map ( ( key , oldIdx ) => [ oldIdx , newStaticAccountKeys . indexOf ( key ) ] ) ) ;
195+ const remappedInstructions = data . versionedInstructions . map ( ( inst ) => ( {
196+ programIdIndex : indexMap . get ( inst . programIdIndex ) ?? inst . programIdIndex ,
197+ accountKeyIndexes : inst . accountKeyIndexes . map ( ( idx ) => indexMap . get ( idx ) ?? idx ) ,
198+ data : inst . data ,
199+ } ) ) ;
200+
201+ return {
202+ ...data ,
203+ versionedInstructions : [ nonceAdvanceInstruction , ...remappedInstructions ] ,
204+ staticAccountKeys : newStaticAccountKeys ,
205+ messageHeader : {
206+ ...data . messageHeader ,
207+ numRequiredSignatures : originalSigners . length ,
208+ } ,
209+ } ;
210+ }
211+
128212 /**
129213 * Clear all custom instructions and versioned transaction data
130214 * @returns This builder instance
0 commit comments