@@ -694,4 +694,303 @@ describe('Signature Index Handling - AVAX P Alignment', () => {
694694 fullSignedTx . id . should . equal ( importPTestData . txhash ) ;
695695 } ) ;
696696 } ) ;
697+
698+ /**
699+ * Test suite for UTXO reordering fix.
700+ *
701+ * FlareJS's newImportTx/newExportTx functions sort inputs by UTXO ID (txid + outputidx)
702+ * for deterministic transaction building. The SDK must match inputs back to UTXOs
703+ * by UTXO ID, not by array index, to ensure credentials are created for the correct inputs.
704+ *
705+ * These tests verify that transactions with multiple UTXOs work correctly regardless
706+ * of the order in which UTXOs are provided.
707+ */
708+ describe ( 'UTXO Reordering Fix - Multiple UTXOs with Different txids' , ( ) => {
709+ describe ( 'ImportInC with reordered UTXOs' , ( ) => {
710+ it ( 'should correctly handle multiple UTXOs that may get reordered by FlareJS' , async ( ) => {
711+ const reorderedUtxos = [ importCTestData . utxos [ 4 ] , importCTestData . utxos [ 0 ] ] ;
712+
713+ const txBuilder = newFactory ( )
714+ . getImportInCBuilder ( )
715+ . threshold ( importCTestData . threshold )
716+ . fromPubKey ( importCTestData . pAddresses )
717+ . decodedUtxos ( reorderedUtxos )
718+ . to ( importCTestData . to )
719+ . fee ( importCTestData . fee )
720+ . context ( importCTestData . context ) ;
721+
722+ txBuilder . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
723+ txBuilder . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
724+
725+ const tx = await txBuilder . build ( ) ;
726+ const txJson = tx . toJson ( ) ;
727+
728+ txJson . signatures . length . should . equal ( 2 ) ;
729+ tx . toBroadcastFormat ( ) . should . be . a . String ( ) ;
730+ txJson . inputs . length . should . equal ( 2 ) ;
731+ } ) ;
732+
733+ it ( 'should correctly sign in parse-sign-parse-sign flow with multiple UTXOs' , async ( ) => {
734+ const reorderedUtxos = [ importCTestData . utxos [ 3 ] , importCTestData . utxos [ 1 ] ] ;
735+
736+ const builder1 = newFactory ( )
737+ . getImportInCBuilder ( )
738+ . threshold ( importCTestData . threshold )
739+ . fromPubKey ( importCTestData . pAddresses )
740+ . decodedUtxos ( reorderedUtxos )
741+ . to ( importCTestData . to )
742+ . fee ( importCTestData . fee )
743+ . context ( importCTestData . context ) ;
744+
745+ const unsignedTx = await builder1 . build ( ) ;
746+ unsignedTx . toJson ( ) . signatures . length . should . equal ( 0 ) ;
747+
748+ const builder2 = newFactory ( ) . from ( unsignedTx . toBroadcastFormat ( ) ) ;
749+ builder2 . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
750+ const halfSignedTx = await builder2 . build ( ) ;
751+ halfSignedTx . toJson ( ) . signatures . length . should . equal ( 1 ) ;
752+
753+ const builder3 = newFactory ( ) . from ( halfSignedTx . toBroadcastFormat ( ) ) ;
754+ builder3 . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
755+ const fullSignedTx = await builder3 . build ( ) ;
756+ fullSignedTx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
757+
758+ fullSignedTx . toBroadcastFormat ( ) . should . be . a . String ( ) ;
759+ fullSignedTx . id . should . be . a . String ( ) ;
760+ } ) ;
761+
762+ it ( 'should handle 3+ UTXOs with different ordering' , async ( ) => {
763+ const mixedUtxos = [ importCTestData . utxos [ 2 ] , importCTestData . utxos [ 4 ] , importCTestData . utxos [ 0 ] ] ;
764+
765+ const txBuilder = newFactory ( )
766+ . getImportInCBuilder ( )
767+ . threshold ( importCTestData . threshold )
768+ . fromPubKey ( importCTestData . pAddresses )
769+ . decodedUtxos ( mixedUtxos )
770+ . to ( importCTestData . to )
771+ . fee ( importCTestData . fee )
772+ . context ( importCTestData . context ) ;
773+
774+ txBuilder . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
775+ txBuilder . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
776+
777+ const tx = await txBuilder . build ( ) ;
778+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
779+ tx . toJson ( ) . inputs . length . should . equal ( 3 ) ;
780+ } ) ;
781+
782+ it ( 'should handle all 5 UTXOs from test data' , async ( ) => {
783+ const allUtxosReversed = [ ...importCTestData . utxos ] . reverse ( ) ;
784+
785+ const txBuilder = newFactory ( )
786+ . getImportInCBuilder ( )
787+ . threshold ( importCTestData . threshold )
788+ . fromPubKey ( importCTestData . pAddresses )
789+ . decodedUtxos ( allUtxosReversed )
790+ . to ( importCTestData . to )
791+ . fee ( importCTestData . fee )
792+ . context ( importCTestData . context ) ;
793+
794+ txBuilder . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
795+ txBuilder . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
796+
797+ const tx = await txBuilder . build ( ) ;
798+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
799+ tx . toJson ( ) . inputs . length . should . equal ( 5 ) ;
800+ } ) ;
801+ } ) ;
802+
803+ describe ( 'ImportInP with multiple UTXOs' , ( ) => {
804+ it ( 'should correctly handle multiple UTXOs with different outputidx' , async ( ) => {
805+ const multipleUtxos = [
806+ {
807+ ...importPTestData . utxos [ 0 ] ,
808+ outputidx : '1' ,
809+ amount : '25000000' ,
810+ } ,
811+ {
812+ ...importPTestData . utxos [ 0 ] ,
813+ outputidx : '0' ,
814+ amount : '25000000' ,
815+ } ,
816+ ] ;
817+
818+ const txBuilder = newFactory ( )
819+ . getImportInPBuilder ( )
820+ . threshold ( importPTestData . threshold )
821+ . locktime ( importPTestData . locktime )
822+ . fromPubKey ( importPTestData . corethAddresses )
823+ . to ( importPTestData . pAddresses )
824+ . externalChainId ( importPTestData . sourceChainId )
825+ . feeState ( importPTestData . feeState )
826+ . context ( importPTestData . context )
827+ . decodedUtxos ( multipleUtxos ) ;
828+
829+ txBuilder . sign ( { key : importPTestData . privateKeys [ 2 ] } ) ;
830+ txBuilder . sign ( { key : importPTestData . privateKeys [ 0 ] } ) ;
831+
832+ const tx = await txBuilder . build ( ) ;
833+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
834+ tx . toJson ( ) . inputs . length . should . equal ( 2 ) ;
835+ } ) ;
836+
837+ it ( 'should correctly sign in parse-sign-parse-sign flow with multiple UTXOs' , async ( ) => {
838+ const multipleUtxos = [
839+ {
840+ ...importPTestData . utxos [ 0 ] ,
841+ outputidx : '1' ,
842+ amount : '25000000' ,
843+ } ,
844+ {
845+ ...importPTestData . utxos [ 0 ] ,
846+ outputidx : '0' ,
847+ amount : '25000000' ,
848+ } ,
849+ ] ;
850+
851+ const builder1 = newFactory ( )
852+ . getImportInPBuilder ( )
853+ . threshold ( importPTestData . threshold )
854+ . locktime ( importPTestData . locktime )
855+ . fromPubKey ( importPTestData . corethAddresses )
856+ . to ( importPTestData . pAddresses )
857+ . externalChainId ( importPTestData . sourceChainId )
858+ . feeState ( importPTestData . feeState )
859+ . context ( importPTestData . context )
860+ . decodedUtxos ( multipleUtxos ) ;
861+
862+ const unsignedTx = await builder1 . build ( ) ;
863+
864+ const builder2 = newFactory ( ) . from ( unsignedTx . toBroadcastFormat ( ) ) ;
865+ builder2 . sign ( { key : importPTestData . privateKeys [ 2 ] } ) ;
866+ const halfSignedTx = await builder2 . build ( ) ;
867+
868+ const builder3 = newFactory ( ) . from ( halfSignedTx . toBroadcastFormat ( ) ) ;
869+ builder3 . sign ( { key : importPTestData . privateKeys [ 0 ] } ) ;
870+ const fullSignedTx = await builder3 . build ( ) ;
871+
872+ fullSignedTx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
873+ fullSignedTx . toBroadcastFormat ( ) . should . be . a . String ( ) ;
874+ } ) ;
875+ } ) ;
876+
877+ describe ( 'ExportInP with multiple UTXOs' , ( ) => {
878+ it ( 'should correctly handle multiple UTXOs that may get reordered' , async ( ) => {
879+ const reorderedUtxos = [
880+ {
881+ ...exportPTestData . utxos [ 1 ] ,
882+ outputidx : '1' ,
883+ } ,
884+ {
885+ ...exportPTestData . utxos [ 1 ] ,
886+ outputidx : '0' ,
887+ } ,
888+ ] ;
889+
890+ const txBuilder = newFactory ( )
891+ . getExportInPBuilder ( )
892+ . threshold ( exportPTestData . threshold )
893+ . locktime ( exportPTestData . locktime )
894+ . fromPubKey ( exportPTestData . pAddresses )
895+ . amount ( '20000000' )
896+ . externalChainId ( exportPTestData . sourceChainId )
897+ . feeState ( exportPTestData . feeState )
898+ . context ( exportPTestData . context )
899+ . decodedUtxos ( reorderedUtxos ) ;
900+
901+ txBuilder . sign ( { key : exportPTestData . privateKeys [ 2 ] } ) ;
902+ txBuilder . sign ( { key : exportPTestData . privateKeys [ 0 ] } ) ;
903+
904+ const tx = await txBuilder . build ( ) ;
905+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
906+ } ) ;
907+
908+ it ( 'should correctly sign in parse-sign-parse-sign flow with multiple UTXOs' , async ( ) => {
909+ const reorderedUtxos = [
910+ {
911+ ...exportPTestData . utxos [ 1 ] ,
912+ outputidx : '1' ,
913+ } ,
914+ {
915+ ...exportPTestData . utxos [ 1 ] ,
916+ outputidx : '0' ,
917+ } ,
918+ ] ;
919+
920+ const builder1 = newFactory ( )
921+ . getExportInPBuilder ( )
922+ . threshold ( exportPTestData . threshold )
923+ . locktime ( exportPTestData . locktime )
924+ . fromPubKey ( exportPTestData . pAddresses )
925+ . amount ( '20000000' )
926+ . externalChainId ( exportPTestData . sourceChainId )
927+ . feeState ( exportPTestData . feeState )
928+ . context ( exportPTestData . context )
929+ . decodedUtxos ( reorderedUtxos ) ;
930+
931+ const unsignedTx = await builder1 . build ( ) ;
932+
933+ const builder2 = newFactory ( ) . from ( unsignedTx . toBroadcastFormat ( ) ) ;
934+ builder2 . sign ( { key : exportPTestData . privateKeys [ 2 ] } ) ;
935+ const halfSignedTx = await builder2 . build ( ) ;
936+
937+ const builder3 = newFactory ( ) . from ( halfSignedTx . toBroadcastFormat ( ) ) ;
938+ builder3 . sign ( { key : exportPTestData . privateKeys [ 0 ] } ) ;
939+ const fullSignedTx = await builder3 . build ( ) ;
940+
941+ fullSignedTx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
942+ fullSignedTx . toBroadcastFormat ( ) . should . be . a . String ( ) ;
943+ } ) ;
944+ } ) ;
945+
946+ describe ( 'Edge cases for UTXO matching' , ( ) => {
947+ it ( 'should match UTXOs by both txid AND outputidx' , async ( ) => {
948+ const sameIdUtxos = [
949+ {
950+ ...importCTestData . utxos [ 0 ] ,
951+ outputidx : '2' ,
952+ } ,
953+ {
954+ ...importCTestData . utxos [ 0 ] ,
955+ outputidx : '0' ,
956+ } ,
957+ ] ;
958+
959+ const txBuilder = newFactory ( )
960+ . getImportInCBuilder ( )
961+ . threshold ( importCTestData . threshold )
962+ . fromPubKey ( importCTestData . pAddresses )
963+ . decodedUtxos ( sameIdUtxos )
964+ . to ( importCTestData . to )
965+ . fee ( importCTestData . fee )
966+ . context ( importCTestData . context ) ;
967+
968+ txBuilder . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
969+ txBuilder . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
970+
971+ const tx = await txBuilder . build ( ) ;
972+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
973+ tx . toJson ( ) . inputs . length . should . equal ( 2 ) ;
974+ } ) ;
975+
976+ it ( 'should work correctly when UTXOs are already in sorted order' , async ( ) => {
977+ const sortedUtxos = [ importCTestData . utxos [ 0 ] , importCTestData . utxos [ 1 ] ] ;
978+
979+ const txBuilder = newFactory ( )
980+ . getImportInCBuilder ( )
981+ . threshold ( importCTestData . threshold )
982+ . fromPubKey ( importCTestData . pAddresses )
983+ . decodedUtxos ( sortedUtxos )
984+ . to ( importCTestData . to )
985+ . fee ( importCTestData . fee )
986+ . context ( importCTestData . context ) ;
987+
988+ txBuilder . sign ( { key : importCTestData . privateKeys [ 2 ] } ) ;
989+ txBuilder . sign ( { key : importCTestData . privateKeys [ 0 ] } ) ;
990+
991+ const tx = await txBuilder . build ( ) ;
992+ tx . toJson ( ) . signatures . length . should . equal ( 2 ) ;
993+ } ) ;
994+ } ) ;
995+ } ) ;
697996} ) ;
0 commit comments