Skip to content

Commit 45f9fb2

Browse files
committed
fix(sdk-coin-flrp): signature ordering according to flareJS lib
Ticket: WIN-8692
1 parent 9b284f3 commit 45f9fb2

8 files changed

Lines changed: 547 additions & 12 deletions

File tree

modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,27 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
178178
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
179179
}
180180

181+
const transferInput = input.input as TransferInput;
182+
const actualSigIndices = transferInput.sigIndicies();
183+
181184
return {
182185
...originalUtxo,
183186
addressesIndex: originalUtxo.addressesIndex,
184187
addresses: originalUtxo.addresses,
185188
threshold: originalUtxo.threshold || this.transaction._threshold,
189+
actualSigIndices,
186190
};
187191
});
188192

189193
this.transaction._utxos = utxosWithIndex;
190194

191-
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
195+
const txCredentials = utxosWithIndex.map((utxo) =>
196+
this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
197+
);
192198

193-
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
199+
const addressMaps = utxosWithIndex.map((utxo) =>
200+
this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
201+
);
194202

195203
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
196204

modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,27 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
178178
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
179179
}
180180

181+
const transferInput = input.input as TransferInput;
182+
const actualSigIndices = transferInput.sigIndicies();
183+
181184
return {
182185
...originalUtxo,
183186
addressesIndex: originalUtxo.addressesIndex,
184187
addresses: originalUtxo.addresses,
185188
threshold: originalUtxo.threshold || this.transaction._threshold,
189+
actualSigIndices,
186190
};
187191
});
188192

189193
this.transaction._utxos = utxosWithIndex;
190194

191-
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
195+
const txCredentials = utxosWithIndex.map((utxo) =>
196+
this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
197+
);
192198

193-
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
199+
const addressMaps = utxosWithIndex.map((utxo) =>
200+
this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
201+
);
194202

195203
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
196204

modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,27 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
209209
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
210210
}
211211

212+
const transferInput = input.input as TransferInput;
213+
const actualSigIndices = transferInput.sigIndicies();
214+
212215
return {
213216
...originalUtxo,
214217
addressesIndex: originalUtxo.addressesIndex,
215218
addresses: originalUtxo.addresses,
216219
threshold: originalUtxo.threshold || this.transaction._threshold,
220+
actualSigIndices,
217221
};
218222
});
219223

220224
this.transaction._utxos = utxosWithIndex;
221225

222-
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
226+
const txCredentials = utxosWithIndex.map((utxo) =>
227+
this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
228+
);
223229

224-
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
230+
const addressMaps = utxosWithIndex.map((utxo) =>
231+
this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
232+
);
225233

226234
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
227235

modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,134 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
288288

289289
return addressMap;
290290
}
291+
292+
/**
293+
* Create credential using the ACTUAL sigIndices from FlareJS.
294+
*
295+
* This method determines which sender addresses correspond to which sigIndex positions,
296+
* then creates the credential with signatures in the correct order matching the sigIndices.
297+
*
298+
* sigIndices tell us which positions in the UTXO's owner addresses need to sign.
299+
* We need to figure out which sender addresses are at those positions and create
300+
* signature slots in the same order as sigIndices.
301+
*
302+
* @param utxo - The UTXO to create credential for
303+
* @param threshold - Number of signatures required
304+
* @param actualSigIndices - The actual sigIndices from FlareJS's built input
305+
* @returns Credential with signatures ordered to match sigIndices
306+
* @protected
307+
*/
308+
protected createCredentialForUtxoWithSigIndices(
309+
utxo: DecodedUtxoObj,
310+
threshold: number,
311+
actualSigIndices: number[]
312+
): Credential {
313+
const sender = this.transaction._fromAddresses;
314+
const addressesIndex = utxo.addressesIndex ?? [];
315+
316+
// either user (0) or recovery (2)
317+
const firstIndex = this.recoverSigner ? 2 : 0;
318+
const bitgoIndex = 1;
319+
320+
if (threshold === 1) {
321+
if (sender && sender.length > firstIndex && addressesIndex[firstIndex] !== undefined) {
322+
return new Credential([utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex'))]);
323+
}
324+
return new Credential([utils.createNewSig('')]);
325+
}
326+
327+
// For threshold >= 2, use the actual sigIndices order from FlareJS
328+
// sigIndices[i] = position in UTXO's owner addresses that needs to sign
329+
// addressesIndex[senderIdx] = position in UTXO's owner addresses for that sender
330+
//
331+
// We need to find which sender corresponds to each sigIndex and create signatures
332+
// in the sigIndices order.
333+
if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) {
334+
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
335+
336+
for (const sigIdx of actualSigIndices) {
337+
// Find which sender address is at this UTXO position
338+
// addressesIndex[senderIdx] tells us which UTXO position each sender is at
339+
const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx);
340+
341+
if (senderIdx === bitgoIndex) {
342+
// This sigIndex slot is for BitGo (HSM) - empty signature
343+
emptySignatures.push(utils.createNewSig(''));
344+
} else if (senderIdx === firstIndex) {
345+
// This sigIndex slot is for user/recovery - embed their address
346+
emptySignatures.push(utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')));
347+
} else {
348+
// Fallback for unknown sender - empty signature
349+
emptySignatures.push(utils.createNewSig(''));
350+
}
351+
}
352+
353+
return new Credential(emptySignatures);
354+
}
355+
356+
// Fallback: create threshold empty signatures
357+
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
358+
for (let i = 0; i < threshold; i++) {
359+
emptySignatures.push(utils.createNewSig(''));
360+
}
361+
return new Credential(emptySignatures);
362+
}
363+
364+
/**
365+
* Create AddressMap using the ACTUAL sigIndices from FlareJS.
366+
*
367+
* Maps sender addresses to signature slots based on the actual sigIndices order.
368+
*
369+
* @param utxo - The UTXO to create AddressMap for
370+
* @param threshold - Number of signatures required
371+
* @param actualSigIndices - The actual sigIndices from FlareJS's built input
372+
* @returns AddressMap that maps addresses to signature slots
373+
* @protected
374+
*/
375+
protected createAddressMapForUtxoWithSigIndices(
376+
utxo: DecodedUtxoObj,
377+
threshold: number,
378+
actualSigIndices: number[]
379+
): FlareUtils.AddressMap {
380+
const addressMap = new FlareUtils.AddressMap();
381+
const sender = this.transaction._fromAddresses;
382+
const addressesIndex = utxo.addressesIndex ?? [];
383+
384+
const firstIndex = this.recoverSigner ? 2 : 0;
385+
const bitgoIndex = 1;
386+
387+
if (threshold === 1) {
388+
if (sender && sender.length > firstIndex) {
389+
addressMap.set(new Address(sender[firstIndex]), 0);
390+
} else if (sender && sender.length > 0) {
391+
addressMap.set(new Address(sender[0]), 0);
392+
}
393+
return addressMap;
394+
}
395+
396+
// For threshold >= 2, map addresses based on actual sigIndices order
397+
if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) {
398+
actualSigIndices.forEach((sigIdx, slotIdx) => {
399+
// Find which sender is at this UTXO position
400+
const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx);
401+
402+
if (senderIdx === bitgoIndex) {
403+
addressMap.set(new Address(sender[bitgoIndex]), slotIdx);
404+
} else if (senderIdx === firstIndex) {
405+
addressMap.set(new Address(sender[firstIndex]), slotIdx);
406+
}
407+
});
408+
409+
return addressMap;
410+
}
411+
412+
// Fallback
413+
if (sender && sender.length >= threshold) {
414+
sender.slice(0, threshold).forEach((addr, i) => {
415+
addressMap.set(new Address(addr), i);
416+
});
417+
}
418+
419+
return addressMap;
420+
}
291421
}

modules/sdk-coin-flrp/test/resources/transactionData/exportInP.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export const EXPORT_IN_P = {
44
txhash: '2R4iE6sX6BtAeTNrVdzifczs7qRGQw3yiaFTXaw9j9A4R6D5FW',
55
// Unsigned tx from script (with empty signatures + credential structure)
66
unsignedHex:
7-
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000360f0650',
7+
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650012845ffc4',
88
halfSigntxHex:
9-
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000184918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a00000000090000000284918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e39df12d',
9+
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000184918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a00000000090000000284918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001f945a1ff',
1010
fullSigntxHex:
1111
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000184918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a00000000090000000284918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a0067902f8f061a628182a3ec1d768b100a3d13fcc01966a993c3300bf3cb5d3dc32870442f83c67741d22f5e7a1168f5ef7f22f26d7b62a183528fcbfd0fdede9300f1e3d1ac',
1212
amount: '55000000', // 0.055 (0.05 FLR + 0.005 FLR fee)

0 commit comments

Comments
 (0)