1- use base64:: Engine ;
21use engine_core:: {
32 credentials:: SigningCredential ,
43 error:: { EngineError , SolanaRpcErrorToEngineError } ,
@@ -12,16 +11,13 @@ use solana_client::{
1211 rpc_config:: { RpcSendTransactionConfig , RpcTransactionConfig } ,
1312} ;
1413use solana_commitment_config:: { CommitmentConfig , CommitmentLevel } ;
15- use solana_sdk:: {
16- hash:: Hash ,
17- pubkey:: Pubkey ,
18- transaction:: VersionedTransaction ,
19- } ;
14+ use solana_sdk:: pubkey:: Pubkey ;
2015use solana_transaction_status:: {
2116 EncodedTransactionWithStatusMeta , UiTransactionEncoding
2217} ;
2318use spl_memo_interface:: instruction:: build_memo;
2419use std:: { sync:: Arc , time:: Duration } ;
20+ use base64:: Engine ;
2521use tracing:: { error, info, warn} ;
2622use twmq:: {
2723 DurableExecution , FailHookData , NackHookData , Queue , SuccessHookData , UserCancellable ,
@@ -437,99 +433,50 @@ impl SolanaExecutorJobHandler {
437433 Ok ( result)
438434 }
439435
440- fn get_writable_accounts (
441- & self ,
442- transaction : & SolanaTransactionOptions ,
443- ) -> Vec < Pubkey > {
444- transaction
445- . instructions
436+ fn get_writable_accounts ( instructions : & [ SolanaInstructionData ] ) -> Vec < Pubkey > {
437+ instructions
446438 . iter ( )
447- . flat_map ( |i | {
448- i . accounts
439+ . flat_map ( |inst | {
440+ inst . accounts
449441 . iter ( )
450442 . filter ( |a| a. is_writable )
451443 . map ( |a| a. pubkey )
452444 } )
453445 . collect ( )
454446 }
455447
456- /// Compile a Solana transaction with priority fees and memo instruction
457- /// ALWAYS NACK on error - network operations can be retried
458- ///
459- /// Adds a memo instruction with the transaction_id to ensure unique signatures
460- /// even when rapidly resubmitting with the same blockhash
461- async fn compile_transaction (
448+ async fn get_compute_unit_price (
462449 & self ,
463- transaction : & SolanaTransactionOptions ,
450+ priority_fee : & SolanaPriorityFee ,
451+ instructions : & [ SolanaInstructionData ] ,
464452 rpc_client : & RpcClient ,
465- recent_blockhash : Hash ,
466453 chain_id : & str ,
467- transaction_id : & str ,
468- ) -> JobResult < VersionedTransaction , SolanaExecutorError > {
469- let compute_unit_price = if let Some ( price_config) = & transaction. execution_options . priority_fee {
470- let price = match price_config {
471- SolanaPriorityFee :: Auto => {
472- self . get_percentile_compute_unit_price (
473- rpc_client,
474- & self . get_writable_accounts ( transaction) ,
475- 75 ,
476- chain_id,
477- )
478- . await ?
479- }
480- SolanaPriorityFee :: Manual { micro_lamports_per_unit } => {
481- * micro_lamports_per_unit
482- }
483- SolanaPriorityFee :: Percentile { percentile } => {
484- self . get_percentile_compute_unit_price (
485- rpc_client,
486- & self . get_writable_accounts ( transaction) ,
487- * percentile,
488- chain_id,
489- )
490- . await ?
491- }
492- } ;
493- Some ( price)
494- } else {
495- None
496- } ;
497-
498- // Add memo instruction with transaction_id for unique signatures
499- let memo_data = format ! ( "thirdweb-engine:{}" , transaction_id) ;
500- let memo_ix = build_memo ( & spl_memo_interface:: v3:: id ( ) , memo_data. as_bytes ( ) , & [ ] ) ;
501-
502- let mut instructions = transaction. instructions . clone ( ) ;
503- let memo_data_base64 = base64:: engine:: general_purpose:: STANDARD . encode ( memo_data. as_bytes ( ) ) ;
504- instructions. push ( SolanaInstructionData {
505- program_id : memo_ix. program_id ,
506- accounts : vec ! [ ] ,
507- data : memo_data_base64,
508- encoding : InstructionDataEncoding :: Base64 ,
509- } ) ;
510-
511- let solana_transaction = SolanaTransaction {
512- instructions,
513- compute_unit_price,
514- compute_unit_limit : transaction. execution_options . compute_unit_limit ,
515- recent_blockhash,
516- } ;
517-
518- let versioned_tx = solana_transaction
519- . to_versioned_transaction ( transaction. execution_options . signer_address , recent_blockhash)
520- . map_err ( |e| {
521- error ! (
522- transaction_id = %transaction_id,
523- error = %e,
524- "Failed to build transaction"
525- ) ;
526- SolanaExecutorError :: TransactionBuildFailed {
527- inner_error : e. to_string ( ) ,
528- }
529- . fail ( )
530- } ) ?;
454+ ) -> JobResult < u64 , SolanaExecutorError > {
455+ let writable_accounts = Self :: get_writable_accounts ( instructions) ;
531456
532- Ok ( versioned_tx)
457+ match priority_fee {
458+ SolanaPriorityFee :: Auto => {
459+ self . get_percentile_compute_unit_price (
460+ rpc_client,
461+ & writable_accounts,
462+ 75 ,
463+ chain_id,
464+ )
465+ . await
466+ }
467+ SolanaPriorityFee :: Manual { micro_lamports_per_unit } => {
468+ Ok ( * micro_lamports_per_unit)
469+ }
470+ SolanaPriorityFee :: Percentile { percentile } => {
471+ self . get_percentile_compute_unit_price (
472+ rpc_client,
473+ & writable_accounts,
474+ * percentile,
475+ chain_id,
476+ )
477+ . await
478+ }
479+ }
533480 }
534481
535482
@@ -650,7 +597,39 @@ impl SolanaExecutorJobHandler {
650597 . nack ( Some ( CONFIRMATION_RETRY_DELAY ) , RequeuePosition :: Last ) ) ;
651598 }
652599 Ok ( false ) => {
653- // Blockhash expired, need to resubmit
600+ // Blockhash expired
601+
602+ // For serialized transactions with existing signatures, we cannot retry with a new blockhash
603+ // because the signatures will become invalid. Check if there are any non-default signatures.
604+ if let engine_solana_core:: transaction:: SolanaTransactionInput :: Serialized ( t) = & job_data. transaction . input {
605+ // Deserialize the base64 transaction to check for signatures
606+ if let Ok ( tx_bytes) = base64:: engine:: general_purpose:: STANDARD . decode ( & t. transaction )
607+ && let Ok ( ( versioned_tx, _) ) = bincode:: serde:: decode_from_slice :: < solana_sdk:: transaction:: VersionedTransaction , _ > (
608+ & tx_bytes,
609+ bincode:: config:: standard ( )
610+ ) {
611+ // Check if any signatures are non-default (not all zeros)
612+ let has_signatures = versioned_tx. signatures . iter ( ) . any ( |sig| {
613+ sig. as_ref ( ) != [ 0u8 ; 64 ]
614+ } ) ;
615+
616+ if has_signatures {
617+ error ! (
618+ transaction_id = %transaction_id,
619+ signature = %signature,
620+ "Blockhash expired for serialized transaction with existing signatures - cannot retry without invalidating them"
621+ ) ;
622+ let _ = self . storage . delete_attempt ( transaction_id) . await ;
623+ return Err ( SolanaExecutorError :: TransactionFailed {
624+ reason : "Blockhash expired for serialized transaction with existing signatures. Retrying with a new blockhash would invalidate them." . to_string ( ) ,
625+ }
626+ . fail ( ) ) ;
627+ }
628+ // If no signatures, we can retry - will be signed during execution
629+ }
630+ }
631+
632+ // For instruction-based transactions or serialized without signatures, we can retry with a new blockhash
654633 warn ! (
655634 transaction_id = %transaction_id,
656635 signature = %signature,
@@ -767,16 +746,73 @@ impl SolanaExecutorJobHandler {
767746 . nack ( Some ( NETWORK_ERROR_RETRY_DELAY ) , RequeuePosition :: Last )
768747 } ) ?;
769748
770- // Compile and sign transaction
771- let versioned_tx = self
772- . compile_transaction (
773- & job_data. transaction ,
774- rpc_client,
775- recent_blockhash,
776- chain_id_str. as_str ( ) ,
777- transaction_id,
778- )
779- . await ?;
749+ // Build transaction - handle execution options differently for instructions vs serialized
750+ let versioned_tx = match & job_data. transaction . input {
751+ engine_solana_core:: transaction:: SolanaTransactionInput :: Instructions ( i) => {
752+ // For instruction-based transactions: calculate priority fees and apply execution options
753+ let compute_unit_price = if let Some ( priority_fee) = & job_data. transaction . execution_options . priority_fee {
754+ Some ( self . get_compute_unit_price ( priority_fee, & i. instructions , rpc_client, chain_id_str. as_str ( ) ) . await ?)
755+ } else {
756+ None
757+ } ;
758+
759+ // Add memo instruction with transaction_id for unique signatures
760+ // This ensures that even with the same blockhash, each resubmission has a unique signature
761+ let memo_data = format ! ( "thirdweb-engine:{}" , transaction_id) ;
762+ let memo_ix = build_memo ( & spl_memo_interface:: v3:: id ( ) , memo_data. as_bytes ( ) , & [ ] ) ;
763+
764+ let mut instructions_with_memo = i. instructions . clone ( ) ;
765+ let memo_data_base64 = base64:: engine:: general_purpose:: STANDARD . encode ( memo_data. as_bytes ( ) ) ;
766+ instructions_with_memo. push ( SolanaInstructionData {
767+ program_id : memo_ix. program_id ,
768+ accounts : vec ! [ ] ,
769+ data : memo_data_base64,
770+ encoding : InstructionDataEncoding :: Base64 ,
771+ } ) ;
772+
773+ let solana_tx = SolanaTransaction {
774+ input : engine_solana_core:: transaction:: SolanaTransactionInput :: new_with_instructions ( instructions_with_memo) ,
775+ compute_unit_limit : job_data. transaction . execution_options . compute_unit_limit ,
776+ compute_unit_price,
777+ } ;
778+
779+ solana_tx
780+ . to_versioned_transaction ( signer_address, recent_blockhash)
781+ . map_err ( |e| {
782+ error ! (
783+ transaction_id = %transaction_id,
784+ error = %e,
785+ "Failed to build transaction from instructions"
786+ ) ;
787+ SolanaExecutorError :: TransactionBuildFailed {
788+ inner_error : e. to_string ( ) ,
789+ }
790+ . fail ( )
791+ } ) ?
792+ }
793+ engine_solana_core:: transaction:: SolanaTransactionInput :: Serialized { .. } => {
794+ // For serialized transactions: ignore execution options to avoid invalidating signatures
795+ let solana_tx = SolanaTransaction {
796+ input : job_data. transaction . input . clone ( ) ,
797+ compute_unit_limit : None ,
798+ compute_unit_price : None ,
799+ } ;
800+
801+ solana_tx
802+ . to_versioned_transaction ( signer_address, recent_blockhash)
803+ . map_err ( |e| {
804+ error ! (
805+ transaction_id = %transaction_id,
806+ error = %e,
807+ "Failed to deserialize compiled transaction"
808+ ) ;
809+ SolanaExecutorError :: TransactionBuildFailed {
810+ inner_error : e. to_string ( ) ,
811+ }
812+ . fail ( )
813+ } ) ?
814+ }
815+ } ;
780816
781817 let signed_tx = self
782818 . solana_signer
0 commit comments