@@ -4,6 +4,8 @@ import { TransactionBuilderFactory, DecodedUtxoObj } from '../../../src/lib';
44import { coins } from '@bitgo/statics' ;
55import { IMPORT_IN_C as testData } from '../../resources/transactionData/importInC' ;
66import signFlowTest from './signFlowTestSuit' ;
7+ import { UnsignedTx } from '@flarenetwork/flarejs' ;
8+ import testUtils from '../../../src/lib/utils' ;
79
810describe ( 'Flrp Import In C Tx Builder' , ( ) => {
911 const factory = new TransactionBuilderFactory ( coins . get ( 'tflrp' ) ) ;
@@ -60,5 +62,249 @@ describe('Flrp Import In C Tx Builder', () => {
6062 rawTx . should . equal ( signedImportHex ) ;
6163 tx . id . should . equal ( '2ks9vW1SVWD4KsNPHgXnV5dpJaCcaxVNbQW4H7t9BMDxApGvfa' ) ;
6264 } ) ;
65+
66+ it ( 'should FAIL with unsorted UTXO addresses - demonstrates AddressMap mismatch issue for import in C-chain tx' , async ( ) => {
67+ // This test uses UTXO addresses in UNSORTED order to demonstrate the issue.
68+ // With unsorted addresses, the current implementation will create AddressMaps incorrectly
69+ // because it uses sequential indices, not UTXO address order.
70+ //
71+ // Expected: AddressMap should map addresses to signature slots based on UTXO order (addressesIndex)
72+ // Current (WRONG): AddressMap uses sequential indices (0, 1, 2...)
73+ //
74+ // This test WILL FAIL with current implementation because AddressMaps don't match credential order
75+
76+ // UTXO addresses in UNSORTED order (different from sorted)
77+ // Sorted would be: [0x3329... (smallest), 0x7e91... (middle), 0xc732... (largest)]
78+ // Unsorted: [0xc732... (largest), 0x3329... (smallest), 0x7e91... (middle)]
79+ const unsortedUtxoAddresses = [
80+ '0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91' , // Largest (would be index 2 if sorted)
81+ '0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581' , // Smallest (would be index 0 if sorted)
82+ '0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001' , // Middle (would be index 1 if sorted)
83+ ] ;
84+
85+ // Corresponding P-chain addresses (in same order as _fromAddresses)
86+ const pAddresses = [
87+ 'P-costwo1xv5mulgpe5lt4tnx2ntnylwe79azu9vpja6lut' , // Maps to 0xc732... (UTXO index 0 in unsorted)
88+ 'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu' , // Maps to 0x3329... (UTXO index 1 in unsorted)
89+ 'P-costwo1cueygd7fd37g56s49k3rshqakhp6k8u3adzt6m' , // Maps to 0x7e91... (UTXO index 2 in unsorted)
90+ ] ;
91+
92+ // Create UTXO with UNSORTED addresses
93+ const amount = '500000000' ; // 0.5 FLR
94+ const fee = '5000000' ; // Example fee
95+ const utxoAmount = ( BigInt ( amount ) + BigInt ( fee ) + BigInt ( '10000000' ) ) . toString ( ) ; // amount + fee + some buffer
96+
97+ const utxo : DecodedUtxoObj = {
98+ outputID : 0 ,
99+ amount : utxoAmount ,
100+ txid : '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP' ,
101+ outputidx : '1' ,
102+ addresses : unsortedUtxoAddresses , // UNSORTED order
103+ threshold : 2 ,
104+ } ;
105+
106+ // Build transaction
107+ const txBuilder = factory
108+ . getImportInCBuilder ( )
109+ . threshold ( 2 )
110+ . fromPubKey ( pAddresses )
111+ . utxos ( [ utxo ] )
112+ . to ( testData . to )
113+ . feeRate ( testData . fee ) ;
114+
115+ // Build unsigned transaction
116+ const unsignedTx = await txBuilder . build ( ) ;
117+ const unsignedHex = unsignedTx . toBroadcastFormat ( ) ;
118+
119+ // Get AddressMaps from the ORIGINAL transaction (before parsing)
120+ // The parsed transaction's AddressMap only contains the output address, not _fromAddresses
121+ const originalFlareTx = ( unsignedTx as any ) . _flareTransaction ;
122+ const originalAddressMaps = ( originalFlareTx as any as UnsignedTx ) . addressMaps ;
123+
124+ // Parse it back to inspect AddressMaps and credentials
125+ const parsedBuilder = factory . from ( unsignedHex ) ;
126+ const parsedTx = await parsedBuilder . build ( ) ;
127+ const flareTx = ( parsedTx as any ) . _flareTransaction ;
128+
129+ // Get the input to check sigIndicies (for C-chain imports, inputs are importedInputs)
130+ const importTx = flareTx . tx as any ;
131+ const input = importTx . importedInputs [ 0 ] ;
132+ const sigIndicies = input . sigIndicies ( ) ;
133+
134+ // sigIndicies tells us: sigIndicies[slotIndex] = utxoAddressIndex
135+ // For threshold=2, we need signatures for first 2 addresses in UTXO order
136+ // UTXO order: [0xc732... (index 0), 0x3329... (index 1), 0x7e91... (index 2)]
137+ // So sigIndicies should be [0, 1] meaning: slot 0 = UTXO index 0, slot 1 = UTXO index 1
138+
139+ // Verify sigIndicies are [0, 1] (first 2 addresses in UTXO order, NOT sorted order)
140+ sigIndicies . length . should . equal ( 2 ) ;
141+ sigIndicies [ 0 ] . should . equal ( 0 , 'First signature slot should be UTXO address index 0 (0xc732...)' ) ;
142+ sigIndicies [ 1 ] . should . equal ( 1 , 'Second signature slot should be UTXO address index 1 (0x3329...)' ) ;
143+
144+ // The critical test: Verify that signature slots have embedded addresses based on UTXO order
145+ // With unsorted UTXO addresses, this will FAIL if AddressMaps don't match UTXO order
146+ //
147+ // Parse the credential to see which slots have which embedded addresses
148+ const credential = flareTx . credentials [ 0 ] ;
149+ const signatures = credential . getSignatures ( ) ;
150+
151+ // Extract embedded addresses from signature slots
152+ const embeddedAddresses : string [ ] = [ ] ;
153+ const isEmptySignature = ( signature : string ) : boolean => {
154+ return ! ! signature && testUtils . removeHexPrefix ( signature ) . startsWith ( '0' . repeat ( 90 ) ) ;
155+ } ;
156+
157+ const hasEmbeddedAddress = ( signature : string ) : boolean => {
158+ if ( ! isEmptySignature ( signature ) ) return false ;
159+ const cleanSig = testUtils . removeHexPrefix ( signature ) ;
160+ if ( cleanSig . length < 130 ) return false ;
161+ const embeddedPart = cleanSig . substring ( 90 , 130 ) ;
162+ // Check if embedded part is not all zeros
163+ return embeddedPart !== '0' . repeat ( 40 ) ;
164+ } ;
165+
166+ signatures . forEach ( ( sig : string , slotIndex : number ) => {
167+ if ( hasEmbeddedAddress ( sig ) ) {
168+ // Extract embedded address (after position 90, 40 chars = 20 bytes)
169+ const cleanSig = testUtils . removeHexPrefix ( sig ) ;
170+ const embeddedAddr = cleanSig . substring ( 90 , 130 ) . toLowerCase ( ) ;
171+ embeddedAddresses [ slotIndex ] = '0x' + embeddedAddr ;
172+ }
173+ } ) ;
174+
175+ // Verify: Credentials only embed ONE address (user/recovery), not both
176+ // The embedded address should be based on addressesIndex logic, not sequential order
177+ //
178+ // Compute addressesIndex to determine expected signature order
179+ const utxoAddressBytes = unsortedUtxoAddresses . map ( ( addr ) => testUtils . parseAddress ( addr ) ) ;
180+ const pAddressBytes = pAddresses . map ( ( addr ) => testUtils . parseAddress ( addr ) ) ;
181+
182+ const addressesIndex : number [ ] = [ ] ;
183+ pAddressBytes . forEach ( ( pAddr ) => {
184+ const utxoIndex = utxoAddressBytes . findIndex (
185+ ( uAddr ) => Buffer . compare ( Buffer . from ( uAddr ) , Buffer . from ( pAddr ) ) === 0
186+ ) ;
187+ addressesIndex . push ( utxoIndex ) ;
188+ } ) ;
189+
190+ // firstIndex = 0 (user), bitgoIndex = 1
191+ const firstIndex = 0 ;
192+ const bitgoIndex = 1 ;
193+
194+ // Determine expected signature order based on addressesIndex
195+ const userComesFirst = addressesIndex [ bitgoIndex ] > addressesIndex [ firstIndex ] ;
196+
197+ // Expected credential structure:
198+ // - If user comes first: [userAddress, zeros]
199+ // - If bitgo comes first: [zeros, userAddress]
200+ const userAddressHex = Buffer . from ( pAddressBytes [ firstIndex ] ) . toString ( 'hex' ) . toLowerCase ( ) ;
201+ const expectedUserAddr = '0x' + userAddressHex ;
202+
203+ if ( userComesFirst ) {
204+ // Expected: [userAddress, zeros]
205+ // Slot 0 should have user address (pAddr0 = 0xc732... = UTXO index 0)
206+ if ( embeddedAddresses [ 0 ] ) {
207+ embeddedAddresses [ 0 ]
208+ . toLowerCase ( )
209+ . should . equal (
210+ expectedUserAddr ,
211+ `Slot 0 should have user address (${ expectedUserAddr } ) because user comes first in UTXO order`
212+ ) ;
213+ } else {
214+ throw new Error ( `Slot 0 should have embedded user address, but is empty` ) ;
215+ }
216+ // Slot 1 should be zeros (no embedded address)
217+ if ( embeddedAddresses [ 1 ] ) {
218+ throw new Error ( `Slot 1 should be zeros, but has embedded address: ${ embeddedAddresses [ 1 ] } ` ) ;
219+ }
220+ } else {
221+ // Expected: [zeros, userAddress]
222+ // Slot 0 should be zeros
223+ if ( embeddedAddresses [ 0 ] ) {
224+ throw new Error ( `Slot 0 should be zeros, but has embedded address: ${ embeddedAddresses [ 0 ] } ` ) ;
225+ }
226+ // Slot 1 should have user address
227+ if ( embeddedAddresses [ 1 ] ) {
228+ embeddedAddresses [ 1 ]
229+ . toLowerCase ( )
230+ . should . equal (
231+ expectedUserAddr ,
232+ `Slot 1 should have user address (${ expectedUserAddr } ) because bitgo comes first in UTXO order`
233+ ) ;
234+ } else {
235+ throw new Error ( `Slot 1 should have embedded user address, but is empty` ) ;
236+ }
237+ }
238+
239+ // The key verification: AddressMaps should match the credential order
240+ // Current implementation (WRONG): AddressMaps use sequential indices (0, 1, 2...)
241+ // Expected (CORRECT): AddressMaps should use addressesIndex logic, matching credential order
242+ //
243+ // Get AddressMaps from the ORIGINAL transaction (not parsed, because parsed AddressMap only has output address)
244+ // For C-chain imports, originalFlareTx is EVMUnsignedTx which has addressMaps property
245+
246+ const addressMaps = originalAddressMaps ;
247+ addressMaps . toArray ( ) . length . should . equal ( 1 , 'Should have one AddressMap for one input' ) ;
248+
249+ const addressMap = addressMaps . toArray ( ) [ 0 ] ;
250+
251+ // Expected: Based on addressesIndex logic
252+ // If user comes first: slot 0 = user, slot 1 = bitgo
253+ // If bitgo comes first: slot 0 = bitgo, slot 1 = user
254+ const expectedSlot0Addr = userComesFirst ? pAddressBytes [ firstIndex ] : pAddressBytes [ bitgoIndex ] ;
255+ const expectedSlot1Addr = userComesFirst ? pAddressBytes [ bitgoIndex ] : pAddressBytes [ firstIndex ] ;
256+
257+ // AddressMap maps: Address -> slot index
258+ // We need to check which addresses are mapped to slots 0 and 1
259+ // AddressMap.get() returns the slot index for a given address
260+
261+ // Verify that AddressMap correctly maps addresses based on credential order (UTXO order)
262+ // The AddressMap should map the addresses that appear in credentials to the correct slots
263+ const { Address } = require ( '@flarenetwork/flarejs' ) ;
264+ const expectedSlot0Address = new Address ( expectedSlot0Addr ) ;
265+ const expectedSlot1Address = new Address ( expectedSlot1Addr ) ;
266+ const expectedSlot0FromMap = addressMap . get ( expectedSlot0Address ) ;
267+ const expectedSlot1FromMap = addressMap . get ( expectedSlot1Address ) ;
268+
269+ // Verify that the expected addresses map to the correct slots
270+ if ( expectedSlot0FromMap === undefined ) {
271+ throw new Error ( `Address at UTXO index ${ addressesIndex [ firstIndex ] } not found in AddressMap` ) ;
272+ }
273+ if ( expectedSlot1FromMap === undefined ) {
274+ throw new Error ( `Address at UTXO index ${ addressesIndex [ bitgoIndex ] } not found in AddressMap` ) ;
275+ }
276+ expectedSlot0FromMap . should . equal ( 0 , `Address at UTXO index ${ addressesIndex [ firstIndex ] } should map to slot 0` ) ;
277+ expectedSlot1FromMap . should . equal ( 1 , `Address at UTXO index ${ addressesIndex [ bitgoIndex ] } should map to slot 1` ) ;
278+
279+ // If addressesIndex is not sequential ([0, 1, ...]), verify that sequential mapping is NOT used incorrectly
280+ // Sequential mapping means: pAddresses[0] -> slot 0, pAddresses[1] -> slot 1, regardless of UTXO order
281+ const usesSequentialMapping = addressesIndex [ 0 ] === 0 && addressesIndex [ 1 ] === 1 ;
282+
283+ if ( ! usesSequentialMapping ) {
284+ // Check if AddressMap uses sequential mapping (array order) instead of UTXO order
285+ const sequentialSlot0 = addressMap . get ( new Address ( pAddressBytes [ 0 ] ) ) ;
286+ const sequentialSlot1 = addressMap . get ( new Address ( pAddressBytes [ 1 ] ) ) ;
287+
288+ // Sequential mapping would map pAddresses[0] -> slot 0, pAddresses[1] -> slot 1
289+ // But we want UTXO order mapping based on addressesIndex
290+ const isSequential = sequentialSlot0 === 0 && sequentialSlot1 === 1 ;
291+
292+ // Check if pAddresses[0] and pAddresses[1] are the expected addresses for slots 0 and 1
293+ // If they are, then sequential mapping happens to be correct (by coincidence)
294+ const pAddress0IsExpectedSlot0 =
295+ Buffer . compare ( Buffer . from ( pAddressBytes [ 0 ] ) , Buffer . from ( expectedSlot0Addr ) ) === 0 ;
296+ const pAddress1IsExpectedSlot1 =
297+ Buffer . compare ( Buffer . from ( pAddressBytes [ 1 ] ) , Buffer . from ( expectedSlot1Addr ) ) === 0 ;
298+
299+ // If sequential mapping is used but it's NOT correct (doesn't match expected addresses), fail
300+ if ( isSequential && ( ! pAddress0IsExpectedSlot0 || ! pAddress1IsExpectedSlot1 ) ) {
301+ throw new Error (
302+ `AddressMap uses sequential mapping (array order) but should use UTXO order. ` +
303+ `addressesIndex: [${ addressesIndex . join ( ', ' ) } ]. ` +
304+ `Expected slot 0 = address at UTXO index ${ addressesIndex [ firstIndex ] } , slot 1 = address at UTXO index ${ addressesIndex [ bitgoIndex ] } `
305+ ) ;
306+ }
307+ }
308+ } ) ;
63309 } ) ;
64310} ) ;
0 commit comments