@@ -6,6 +6,7 @@ use crate::types::{
66 SmartTransactionConfig , Timeout ,
77} ;
88use crate :: Helius ;
9+ use std:: collections:: HashSet ;
910use std:: str:: FromStr ;
1011use std:: sync:: Arc ;
1112
@@ -64,6 +65,42 @@ const MIN_TIP_LAMPORTS_DUAL: u64 = 200_000; // 0.0002 SOL
6465/// Minimum tip: 0.000005 SOL (5,000 lamports).
6566const MIN_TIP_LAMPORTS_SWQOS : u64 = 5_000 ; // 0.000005 SOL
6667
68+ fn collect_unique_signers ( signers : & [ Arc < dyn Signer > ] , fee_payer : Option < & Arc < dyn Signer > > ) -> Vec < Arc < dyn Signer > > {
69+ let mut all_signers: Vec < Arc < dyn Signer > > = Vec :: with_capacity ( signers. len ( ) + usize:: from ( fee_payer. is_some ( ) ) ) ;
70+ let mut seen: HashSet < Pubkey > = HashSet :: with_capacity ( all_signers. capacity ( ) ) ;
71+
72+ if let Some ( fee_payer) = fee_payer {
73+ if seen. insert ( fee_payer. pubkey ( ) ) {
74+ all_signers. push ( fee_payer. clone ( ) ) ;
75+ }
76+ }
77+
78+ for signer in signers {
79+ if seen. insert ( signer. pubkey ( ) ) {
80+ all_signers. push ( signer. clone ( ) ) ;
81+ }
82+ }
83+
84+ all_signers
85+ }
86+
87+ fn collect_unique_keypair_refs < ' a > ( signers : & ' a [ Keypair ] , fee_payer : & ' a Keypair ) -> Vec < & ' a Keypair > {
88+ let mut all_signers: Vec < & Keypair > = Vec :: with_capacity ( signers. len ( ) + 1 ) ;
89+ let mut seen: HashSet < Pubkey > = HashSet :: with_capacity ( all_signers. capacity ( ) ) ;
90+
91+ if seen. insert ( fee_payer. pubkey ( ) ) {
92+ all_signers. push ( fee_payer) ;
93+ }
94+
95+ for signer in signers {
96+ if seen. insert ( signer. pubkey ( ) ) {
97+ all_signers. push ( signer) ;
98+ }
99+ }
100+
101+ all_signers
102+ }
103+
67104/// URL to fetch current Jito bundle tip floor prices.
68105///
69106/// This endpoint returns the minimum tip amounts required for different
@@ -355,8 +392,8 @@ impl Helius {
355392 {
356393 return Ok ( txt_sig) ;
357394 }
358- if status. err . is_some ( ) {
359- return Err ( HeliusError :: TransactionError ( status . err . unwrap ( ) ) ) ;
395+ if let Some ( err ) = status. err {
396+ return Err ( HeliusError :: TransactionError ( err) ) ;
360397 }
361398 }
362399 None => {
@@ -451,12 +488,13 @@ impl Helius {
451488 final_instructions. push ( compute_budget_ix) ;
452489
453490 // Get the optimal compute units
491+ let all_signers: Vec < Arc < dyn Signer > > = collect_unique_signers ( & config. signers , config. fee_payer . as_ref ( ) ) ;
454492 let units: Option < u64 > = self
455493 . get_compute_units (
456494 updated_instructions,
457495 payer_pubkey,
458496 config. lookup_tables . clone ( ) . unwrap_or_default ( ) ,
459- Some ( & config . signers ) ,
497+ Some ( & all_signers ) ,
460498 )
461499 . await ?;
462500
@@ -491,38 +529,17 @@ impl Helius {
491529 let v0_message: v0:: Message =
492530 v0:: Message :: try_compile ( & payer_pubkey, & final_instructions, lookup_tables, recent_blockhash) ?;
493531 let versioned_message: VersionedMessage = VersionedMessage :: V0 ( v0_message) ;
494-
495- let all_signers: Vec < Arc < dyn Signer > > = if let Some ( fee_payer) = config. fee_payer . as_ref ( ) {
496- let mut all_signers = config. signers . clone ( ) ;
497- if !all_signers. iter ( ) . any ( |signer| signer. pubkey ( ) == fee_payer. pubkey ( ) ) {
498- all_signers. push ( fee_payer. clone ( ) ) ;
499- }
500- all_signers
501- } else {
502- config. signers . clone ( )
503- } ;
504-
505- let signatures: Vec < Signature > = all_signers
506- . iter ( )
507- . map ( |signer| signer. try_sign_message ( versioned_message. serialize ( ) . as_slice ( ) ) )
508- . collect :: < std:: result:: Result < Vec < _ > , _ > > ( ) ?;
509-
510- let versioned_transaction = VersionedTransaction {
511- signatures,
512- message : versioned_message,
513- } ;
532+ let versioned_transaction: VersionedTransaction =
533+ VersionedTransaction :: try_new ( versioned_message, all_signers. as_slice ( ) )
534+ . map_err ( |e| HeliusError :: InvalidInput ( format ! ( "Signing error: {:?}" , e) ) ) ?;
514535
515536 Ok ( (
516537 SmartTransaction :: Versioned ( versioned_transaction) ,
517538 last_valid_block_hash,
518539 ) )
519540 } else {
520541 let mut tx: Transaction = Transaction :: new_with_payer ( & final_instructions, Some ( & payer_pubkey) ) ;
521- tx. try_partial_sign ( & config. signers , recent_blockhash) ?;
522-
523- if let Some ( fee_payer) = config. fee_payer . as_ref ( ) {
524- tx. try_partial_sign ( & [ fee_payer] , recent_blockhash) ?;
525- }
542+ tx. try_partial_sign ( & all_signers, recent_blockhash) ?;
526543
527544 Ok ( ( SmartTransaction :: Legacy ( tx) , last_valid_block_hash) )
528545 }
@@ -637,16 +654,8 @@ impl Helius {
637654 let versioned_message: VersionedMessage = VersionedMessage :: V0 ( v0_message) ;
638655
639656 let transaction: VersionedTransaction = if let Some ( keypairs) = keypairs {
640- let mut tx = VersionedTransaction {
641- signatures : vec ! [ Signature :: default ( ) ; keypairs. len( ) ] ,
642- message : versioned_message. clone ( ) ,
643- } ;
644-
645- for ( i, keypair) in keypairs. iter ( ) . enumerate ( ) {
646- tx. signatures [ i] = keypair. sign_message ( & versioned_message. serialize ( ) ) ;
647- }
648-
649- tx
657+ VersionedTransaction :: try_new ( versioned_message, keypairs)
658+ . map_err ( |e| HeliusError :: InvalidInput ( format ! ( "Signing error: {:?}" , e) ) ) ?
650659 } else {
651660 VersionedTransaction {
652661 signatures : vec ! [ ] ,
@@ -759,14 +768,7 @@ impl Helius {
759768 let mut test_instructions: Vec < Instruction > = final_instructions. clone ( ) ;
760769 test_instructions. extend ( create_config. instructions . clone ( ) ) ;
761770
762- let mut all_signers: Vec < & Keypair > = vec ! [ & fee_payer] ;
763- let mut seen: std:: collections:: HashSet < Pubkey > = std:: collections:: HashSet :: new ( ) ;
764- seen. insert ( fee_payer. pubkey ( ) ) ;
765- for kp in & keypairs {
766- if seen. insert ( kp. pubkey ( ) ) {
767- all_signers. push ( kp) ;
768- }
769- }
771+ let all_signers: Vec < & Keypair > = collect_unique_keypair_refs ( & keypairs, & fee_payer) ;
770772
771773 let units: Option < u64 > = self
772774 . get_compute_units_thread_safe (
@@ -804,29 +806,13 @@ impl Helius {
804806 ) ?;
805807
806808 let versioned_message: VersionedMessage = VersionedMessage :: V0 ( message) ;
807-
808- let fee_payer_copy: Keypair = fee_payer. insecure_clone ( ) ;
809- let mut all_signers: Vec < Keypair > = vec ! [ fee_payer_copy] ;
810- all_signers. extend ( keypairs. into_iter ( ) . filter ( |k| k. pubkey ( ) != fee_payer. pubkey ( ) ) ) ;
811-
812- let mut tx: VersionedTransaction = VersionedTransaction {
813- signatures : vec ! [ Signature :: default ( ) ; all_signers. len( ) ] ,
814- message : versioned_message. clone ( ) ,
815- } ;
816-
817- // Sign message with all keypairs
818- for ( i, keypair) in all_signers. iter ( ) . enumerate ( ) {
819- tx. signatures [ i] = keypair. sign_message ( & versioned_message. serialize ( ) ) ;
820- }
809+ let tx: VersionedTransaction = VersionedTransaction :: try_new ( versioned_message, all_signers. as_slice ( ) )
810+ . map_err ( |e| HeliusError :: InvalidInput ( format ! ( "Signing error: {:?}" , e) ) ) ?;
821811
822812 SmartTransaction :: Versioned ( tx)
823813 } else {
824814 let mut tx: Transaction = Transaction :: new_with_payer ( & final_instructions, Some ( & fee_payer. pubkey ( ) ) ) ;
825-
826- let mut signers: Vec < & Keypair > = vec ! [ & fee_payer] ;
827- signers. extend ( keypairs. iter ( ) . filter ( |k| k. pubkey ( ) != fee_payer. pubkey ( ) ) ) ;
828-
829- tx. sign ( & signers, recent_blockhash) ;
815+ tx. sign ( & all_signers, recent_blockhash) ;
830816
831817 SmartTransaction :: Legacy ( tx)
832818 } ;
@@ -1210,3 +1196,190 @@ impl Helius {
12101196 }
12111197 }
12121198}
1199+
1200+ #[ cfg( test) ]
1201+ mod tests {
1202+ use super :: { collect_unique_keypair_refs, collect_unique_signers} ;
1203+ use solana_sdk:: {
1204+ hash:: Hash ,
1205+ instruction:: { AccountMeta , Instruction } ,
1206+ message:: { v0, VersionedMessage } ,
1207+ pubkey:: Pubkey ,
1208+ signature:: { Keypair , Signature , Signer } ,
1209+ transaction:: { Transaction , VersionedTransaction } ,
1210+ } ;
1211+ use std:: sync:: Arc ;
1212+
1213+ fn build_versioned_message (
1214+ payer : & Keypair ,
1215+ writable_signer : & Keypair ,
1216+ readonly_signer : & Keypair ,
1217+ ) -> VersionedMessage {
1218+ let instruction = Instruction {
1219+ program_id : Pubkey :: new_unique ( ) ,
1220+ accounts : vec ! [
1221+ AccountMeta :: new( writable_signer. pubkey( ) , true ) ,
1222+ AccountMeta :: new_readonly( readonly_signer. pubkey( ) , true ) ,
1223+ ] ,
1224+ data : vec ! [ ] ,
1225+ } ;
1226+
1227+ VersionedMessage :: V0 (
1228+ v0:: Message :: try_compile ( & payer. pubkey ( ) , & [ instruction] , & [ ] , Hash :: new_unique ( ) ) . unwrap ( ) ,
1229+ )
1230+ }
1231+
1232+ #[ test]
1233+ fn collect_unique_signers_includes_fee_payer_once ( ) {
1234+ let fee_payer: Arc < dyn Signer > = Arc :: new ( Keypair :: new ( ) ) ;
1235+ let signer: Arc < dyn Signer > = Arc :: new ( Keypair :: new ( ) ) ;
1236+
1237+ let signers: Vec < Arc < dyn Signer > > = vec ! [ signer. clone( ) , fee_payer. clone( ) , signer. clone( ) ] ;
1238+ let all_signers = collect_unique_signers ( & signers, Some ( & fee_payer) ) ;
1239+
1240+ let signer_pubkeys: Vec < Pubkey > = all_signers. iter ( ) . map ( |signer| signer. pubkey ( ) ) . collect ( ) ;
1241+ assert_eq ! ( signer_pubkeys, vec![ fee_payer. pubkey( ) , signer. pubkey( ) ] ) ;
1242+ }
1243+
1244+ #[ test]
1245+ fn collect_unique_keypair_refs_includes_fee_payer_once ( ) {
1246+ let fee_payer = Keypair :: new ( ) ;
1247+ let signer = Keypair :: new ( ) ;
1248+ let signers = vec ! [
1249+ signer. insecure_clone( ) ,
1250+ fee_payer. insecure_clone( ) ,
1251+ signer. insecure_clone( ) ,
1252+ ] ;
1253+
1254+ let all_signers = collect_unique_keypair_refs ( & signers, & fee_payer) ;
1255+ let signer_pubkeys: Vec < Pubkey > = all_signers. iter ( ) . map ( |signer| signer. pubkey ( ) ) . collect ( ) ;
1256+
1257+ assert_eq ! ( signer_pubkeys, vec![ fee_payer. pubkey( ) , signer. pubkey( ) ] ) ;
1258+ }
1259+
1260+ #[ test]
1261+ fn versioned_try_new_reorders_arc_signers_to_match_message ( ) {
1262+ let fee_payer = Keypair :: new ( ) ;
1263+ let writable_signer = Keypair :: new ( ) ;
1264+ let readonly_signer = Keypair :: new ( ) ;
1265+ let fee_payer_signer: Arc < dyn Signer > = Arc :: new ( fee_payer. insecure_clone ( ) ) ;
1266+ let writable_signer_arc: Arc < dyn Signer > = Arc :: new ( writable_signer. insecure_clone ( ) ) ;
1267+ let readonly_signer_arc: Arc < dyn Signer > = Arc :: new ( readonly_signer. insecure_clone ( ) ) ;
1268+
1269+ let message = build_versioned_message ( & fee_payer, & writable_signer, & readonly_signer) ;
1270+ let signers: Vec < Arc < dyn Signer > > = vec ! [ readonly_signer_arc. clone( ) , writable_signer_arc. clone( ) ] ;
1271+ let all_signers = collect_unique_signers ( & signers, Some ( & fee_payer_signer) ) ;
1272+ let tx = VersionedTransaction :: try_new ( message. clone ( ) , all_signers. as_slice ( ) ) . unwrap ( ) ;
1273+ let message_bytes = message. serialize ( ) ;
1274+
1275+ assert_eq ! (
1276+ tx. signatures,
1277+ vec![
1278+ Signature :: from( fee_payer. sign_message( & message_bytes) ) ,
1279+ Signature :: from( writable_signer. sign_message( & message_bytes) ) ,
1280+ Signature :: from( readonly_signer. sign_message( & message_bytes) ) ,
1281+ ]
1282+ ) ;
1283+ }
1284+
1285+ #[ test]
1286+ fn manual_fee_payer_appended_signature_order_fails_verification ( ) {
1287+ let fee_payer = Keypair :: new ( ) ;
1288+ let writable_signer = Keypair :: new ( ) ;
1289+ let readonly_signer = Keypair :: new ( ) ;
1290+ let message = build_versioned_message ( & fee_payer, & writable_signer, & readonly_signer) ;
1291+ let message_bytes = message. serialize ( ) ;
1292+
1293+ // This mirrors the pre-fix separate fee payer path: caller signers first, fee payer appended last.
1294+ let manual_signatures = [
1295+ readonly_signer. sign_message ( & message_bytes) ,
1296+ writable_signer. sign_message ( & message_bytes) ,
1297+ fee_payer. sign_message ( & message_bytes) ,
1298+ ] ;
1299+
1300+ let verification_results: Vec < bool > = manual_signatures
1301+ . iter ( )
1302+ . zip ( message. static_account_keys ( ) . iter ( ) )
1303+ . map ( |( signature, pubkey) | signature. verify ( pubkey. as_ref ( ) , & message_bytes) )
1304+ . collect ( ) ;
1305+
1306+ assert_eq ! ( verification_results, vec![ false , true , false ] ) ;
1307+ }
1308+
1309+ #[ test]
1310+ fn manual_non_payer_caller_order_can_fail_verification ( ) {
1311+ let fee_payer = Keypair :: new ( ) ;
1312+ let writable_signer = Keypair :: new ( ) ;
1313+ let readonly_signer = Keypair :: new ( ) ;
1314+ let message = build_versioned_message ( & fee_payer, & writable_signer, & readonly_signer) ;
1315+ let message_bytes = message. serialize ( ) ;
1316+
1317+ // This mirrors the pre-fix seed path: fee payer first, remaining signers left in caller order.
1318+ let manual_signatures = [
1319+ fee_payer. sign_message ( & message_bytes) ,
1320+ readonly_signer. sign_message ( & message_bytes) ,
1321+ writable_signer. sign_message ( & message_bytes) ,
1322+ ] ;
1323+
1324+ let verification_results: Vec < bool > = manual_signatures
1325+ . iter ( )
1326+ . zip ( message. static_account_keys ( ) . iter ( ) )
1327+ . map ( |( signature, pubkey) | signature. verify ( pubkey. as_ref ( ) , & message_bytes) )
1328+ . collect ( ) ;
1329+
1330+ assert_eq ! ( verification_results, vec![ true , false , false ] ) ;
1331+ }
1332+
1333+ #[ test]
1334+ fn versioned_try_new_reorders_keypair_signers_to_match_message ( ) {
1335+ let fee_payer = Keypair :: new ( ) ;
1336+ let writable_signer = Keypair :: new ( ) ;
1337+ let readonly_signer = Keypair :: new ( ) ;
1338+
1339+ let message = build_versioned_message ( & fee_payer, & writable_signer, & readonly_signer) ;
1340+ let signers = vec ! [ readonly_signer. insecure_clone( ) , writable_signer. insecure_clone( ) ] ;
1341+ let all_signers = collect_unique_keypair_refs ( & signers, & fee_payer) ;
1342+ let tx = VersionedTransaction :: try_new ( message. clone ( ) , all_signers. as_slice ( ) ) . unwrap ( ) ;
1343+ let message_bytes = message. serialize ( ) ;
1344+
1345+ assert_eq ! (
1346+ tx. signatures,
1347+ vec![
1348+ Signature :: from( fee_payer. sign_message( & message_bytes) ) ,
1349+ Signature :: from( writable_signer. sign_message( & message_bytes) ) ,
1350+ Signature :: from( readonly_signer. sign_message( & message_bytes) ) ,
1351+ ]
1352+ ) ;
1353+ }
1354+
1355+ #[ test]
1356+ fn legacy_try_partial_sign_reorders_keypairs_to_match_message ( ) {
1357+ let fee_payer = Keypair :: new ( ) ;
1358+ let writable_signer = Keypair :: new ( ) ;
1359+ let readonly_signer = Keypair :: new ( ) ;
1360+ let recent_blockhash = Hash :: new_unique ( ) ;
1361+ let instruction = Instruction {
1362+ program_id : Pubkey :: new_unique ( ) ,
1363+ accounts : vec ! [
1364+ AccountMeta :: new( writable_signer. pubkey( ) , true ) ,
1365+ AccountMeta :: new_readonly( readonly_signer. pubkey( ) , true ) ,
1366+ ] ,
1367+ data : vec ! [ ] ,
1368+ } ;
1369+ let mut tx = Transaction :: new_with_payer ( & [ instruction] , Some ( & fee_payer. pubkey ( ) ) ) ;
1370+ let signers = vec ! [ readonly_signer. insecure_clone( ) , writable_signer. insecure_clone( ) ] ;
1371+ let all_signers = collect_unique_keypair_refs ( & signers, & fee_payer) ;
1372+
1373+ tx. try_partial_sign ( & all_signers, recent_blockhash) . unwrap ( ) ;
1374+
1375+ let message_bytes = tx. message_data ( ) ;
1376+ assert_eq ! (
1377+ tx. signatures,
1378+ vec![
1379+ fee_payer. sign_message( & message_bytes) ,
1380+ writable_signer. sign_message( & message_bytes) ,
1381+ readonly_signer. sign_message( & message_bytes) ,
1382+ ]
1383+ ) ;
1384+ }
1385+ }
0 commit comments