From facc354ba8b6a5109ba2cb7677869d1f19406a61 Mon Sep 17 00:00:00 2001 From: Kashif Jamil Date: Thu, 29 Jan 2026 02:02:10 +0530 Subject: [PATCH] fix(sdk-coin-flrp): update address index computation and signing logic in txn builders Ticket: WIN-8746 --- .../src/lib/ExportInPTxBuilder.ts | 23 +- .../src/lib/ImportInCTxBuilder.ts | 15 +- .../src/lib/ImportInPTxBuilder.ts | 20 +- .../src/lib/atomicTransactionBuilder.ts | 269 +--- .../sdk-coin-flrp/test/resources/account.ts | 22 + .../resources/transactionData/exportInP.ts | 8 +- .../resources/transactionData/importInC.ts | 8 +- .../resources/transactionData/importInP.ts | 8 +- modules/sdk-coin-flrp/test/unit/flrp.ts | 4 +- .../test/unit/lib/exportInPTxBuilder.ts | 309 ++-- .../test/unit/lib/importInCTxBuilder.ts | 261 +--- .../test/unit/lib/importInPTxBuilder.ts | 459 +----- .../test/unit/lib/signatureIndex.ts | 1377 ----------------- 13 files changed, 330 insertions(+), 2453 deletions(-) delete mode 100644 modules/sdk-coin-flrp/test/unit/lib/signatureIndex.ts diff --git a/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts index 8d32ed4d76..cb9710193d 100644 --- a/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts @@ -74,7 +74,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { this.transaction._rawSignedBytes = rawBytes; } - this.computeAddressesIndexFromParsed(); + this.computeAddressesIndex(true); // Use parsed credentials if available, otherwise create new ones based on sigIndices // The sigIndices from the parsed transaction (stored in addressesIndex) determine @@ -87,7 +87,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { const sigIndices = utxo.addressesIndex ?? []; // Use sigIndices-based method if we have valid sigIndices from parsed transaction if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createCredentialForUtxo(utxo, utxoThreshold, sigIndices); } return this.createCredentialForUtxo(utxo, utxoThreshold); }); @@ -97,7 +97,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { const utxoThreshold = utxo.threshold || this.transaction._threshold; const sigIndices = utxo.addressesIndex ?? []; if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createAddressMapForUtxo(utxo, utxoThreshold, sigIndices); } return this.createAddressMapForUtxo(utxo, utxoThreshold); }); @@ -156,19 +156,21 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { } const assetId = utils.flareIdString(this.transaction._assetId).toString(); - const fromAddresses = this.transaction._fromAddresses.map((addr) => Buffer.from(addr)); + const allFromAddresses = this.transaction._fromAddresses.map((addr) => Buffer.from(addr)); const transferableOutput = TransferableOutput.fromNative( assetId, this.transaction._amount, - fromAddresses, + allFromAddresses, this.transaction._locktime, this.transaction._threshold ); + const signingAddresses = this.getSigningAddresses(); + const exportTx = pvm.e.newExportTx( { feeState, - fromAddressesBytes: this.transaction._fromAddresses.map((addr) => Buffer.from(addr)), + fromAddressesBytes: signingAddresses, destinationChainId: this.transaction._network.cChainBlockchainID, outputs: [transferableOutput], utxos: nativeUtxos, @@ -183,15 +185,16 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { let correctedExportTx: pvmSerial.ExportTx = innerTx; if (changeOutputs.length > 0) { + const allWalletAddresses = this.transaction._fromAddresses.map((addr) => Buffer.from(addr)); + const correctedChangeOutputs = changeOutputs.map((output) => { const transferOut = output.output as TransferOutput; - const originalOwners = transferOut.outputOwners; const assetIdStr = utils.flareIdString(Buffer.from(output.assetId.toBytes()).toString('hex')).toString(); return TransferableOutput.fromNative( assetIdStr, transferOut.amount(), - originalOwners.addrs.map((addr) => Buffer.from(addr.toBytes())), + allWalletAddresses, this.transaction._locktime, this.transaction._threshold ); @@ -227,11 +230,11 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { this.transaction._utxos = utxosWithIndex; const txCredentials = utxosWithIndex.map((utxo) => - this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createCredentialForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const addressMaps = utxosWithIndex.map((utxo) => - this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createAddressMapForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const fixedUnsignedTx = new UnsignedTx( diff --git a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts index a259431dd6..263907895d 100644 --- a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts @@ -74,14 +74,14 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { fee: fee.toString(), }; - this.computeAddressesIndexFromParsed(); + this.computeAddressesIndex(true); // Create addressMaps using sigIndices from parsed transaction for consistency const addressMaps = this.transaction._utxos.map((utxo) => { const utxoThreshold = utxo.threshold || this.transaction._threshold; const sigIndices = utxo.addressesIndex ?? []; if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createAddressMapForUtxo(utxo, utxoThreshold, sigIndices); } return this.createAddressMapForUtxo(utxo, utxoThreshold); }); @@ -97,7 +97,7 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { const utxoThreshold = utxo.threshold || this.transaction._threshold; const sigIndices = utxo.addressesIndex ?? []; if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createCredentialForUtxo(utxo, utxoThreshold, sigIndices); } return this.createCredentialForUtxo(utxo, utxoThreshold); }); @@ -165,10 +165,13 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { `Insufficient UTXO balance: have ${totalUtxoAmount.toString()} nFLR, need more than ${actualFeeNFlr.toString()} nFLR to cover import fee` ); } + + const signingAddresses = this.getSigningAddresses(); + const importTx = evm.newImportTx( this.transaction._context, this.transaction._to[0], - this.transaction._fromAddresses.map((addr) => Buffer.from(addr)), + signingAddresses, nativeUtxos, sourceChain, actualFeeNFlr @@ -204,11 +207,11 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { this.transaction._utxos = utxosWithIndex; const txCredentials = utxosWithIndex.map((utxo) => - this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createCredentialForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const addressMaps = utxosWithIndex.map((utxo) => - this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createAddressMapForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials); diff --git a/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts index c2bd1e0d60..4155476586 100644 --- a/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts @@ -93,7 +93,7 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { this.transaction._rawSignedBytes = rawBytes; } - this.computeAddressesIndexFromParsed(); + this.computeAddressesIndex(true); // Use parsed credentials if available, otherwise create new ones based on sigIndices // The sigIndices from the parsed transaction (stored in addressesIndex) determine @@ -106,7 +106,7 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { const sigIndices = utxo.addressesIndex ?? []; // Use sigIndices-based method if we have valid sigIndices from parsed transaction if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createCredentialForUtxo(utxo, utxoThreshold, sigIndices); } return this.createCredentialForUtxo(utxo, utxoThreshold); }); @@ -116,7 +116,7 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { const utxoThreshold = utxo.threshold || this.transaction._threshold; const sigIndices = utxo.addressesIndex ?? []; if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) { - return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices); + return this.createAddressMapForUtxo(utxo, utxoThreshold, sigIndices); } return this.createAddressMapForUtxo(utxo, utxoThreshold); }); @@ -180,24 +180,18 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { } const toAddresses = this.transaction._to.map((addr) => Buffer.from(addr)); - const fromAddresses = this.transaction._fromAddresses.map((addr) => Buffer.from(addr)); const invalidToAddress = toAddresses.find((addr) => addr.length !== 20); if (invalidToAddress) { throw new BuildTransactionError(`Invalid toAddress length: expected 20 bytes, got ${invalidToAddress.length}`); } - const invalidFromAddress = fromAddresses.find((addr) => addr.length !== 20); - if (invalidFromAddress) { - throw new BuildTransactionError( - `Invalid fromAddress length: expected 20 bytes, got ${invalidFromAddress.length}` - ); - } + const signingAddresses = this.getSigningAddresses(); const importTx = pvm.e.newImportTx( { feeState: this.transaction._feeState, - fromAddressesBytes: fromAddresses, + fromAddressesBytes: signingAddresses, sourceChainId: this.transaction._network.cChainBlockchainID, toAddressesBytes: toAddresses, utxos: nativeUtxos, @@ -237,11 +231,11 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { this.transaction._utxos = utxosWithIndex; const txCredentials = utxosWithIndex.map((utxo) => - this.createCredentialForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createCredentialForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const addressMaps = utxosWithIndex.map((utxo) => - this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices) + this.createAddressMapForUtxo(utxo, utxo.threshold, utxo.actualSigIndices) ); const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials); diff --git a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts index 210ab63554..0478012815 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts @@ -101,66 +101,84 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { return this; } + /* + * Key naming: + * A = user key, B = hsm key (BitGo), C = backup key + * + * _fromAddresses (BitGo convention) = [ A, B, C ] at indices [0, 1, 2] + * + * Signing key selection (which keys from _fromAddresses to use): + * - non-recovery: _fromAddresses[0] + _fromAddresses[1] (user + bitgo) + * - recovery: _fromAddresses[1] + _fromAddresses[2] (bitgo + backup) + * + * sigIndices in transaction (positions in sorted UTXO address list): + * - UTXO addresses are sorted by hex value on-chain + * - sigIndices = positions of the 2 signing keys in this sorted list + * - Example: if user sorts to position 2 and bitgo to position 0, + * then sigIndices = [0, 2] (even though we picked _fromAddresses[0, 1]) + */ + /** - * Compute addressesIndex for UTXOs following AVAX P approach. - * addressesIndex[senderIdx] = position of sender[senderIdx] in UTXO's address list + * Get the 2 signing addresses for FlareJS transaction building. * - * IMPORTANT: UTXO addresses are sorted lexicographically by byte value to match - * on-chain storage order. The API may return addresses in arbitrary order, but - * on-chain UTXOs always store addresses in sorted order. + * FlareJS's matchOwners() selects signers in sorted UTXO address order, + * not based on which keys should actually sign. By passing only 2 addresses, + * we ensure the correct signers are selected. * - * Example: - * A = user key, B = hsm key, C = backup key - * sender (bitgoAddresses) = [ A, B, C ] - * utxo.addresses (from API) = [ B, C, A ] - * sorted utxo.addresses = [ A, B, C ] (sorted by hex value) - * addressesIndex = [ 0, 1, 2 ] - * (sender[0]=A is at position 0 in sorted UTXO, sender[1]=B is at position 1, etc.) + * This mirrors AVAXP's approach in createInputOutput() where: + * - For non-recovery: use user (index 0) and bitgo (index 1) + * - For recovery: use bitgo (index 1) and recovery (index 2) * + * @returns Array of 2 signing address buffers * @protected */ - protected computeAddressesIndex(): void { - const sender = this.transaction._fromAddresses; + protected getSigningAddresses(): Buffer[] { + const firstIndex = this.recoverSigner ? 2 : 0; + const bitgoIndex = 1; - this.transaction._utxos.forEach((utxo) => { - if (utxo.addressesIndex && utxo.addressesIndex.length > 0) { - return; - } + if (this.transaction._fromAddresses.length < Math.max(firstIndex, bitgoIndex) + 1) { + throw new BuildTransactionError( + `Insufficient fromAddresses: need at least ${Math.max(firstIndex, bitgoIndex) + 1} addresses` + ); + } - if (utxo.addresses && utxo.addresses.length > 0) { - const sortedAddresses = utils.sortAddressesByHex(utxo.addresses); - utxo.addresses = sortedAddresses; + const signingAddresses = [ + Buffer.from(this.transaction._fromAddresses[firstIndex]), + Buffer.from(this.transaction._fromAddresses[bitgoIndex]), + ]; - const utxoAddresses = sortedAddresses.map((a) => utils.parseAddress(a)); - utxo.addressesIndex = sender.map((a) => - utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0) - ); - } - }); + const invalidAddr = signingAddresses.find((addr) => addr.length !== 20); + if (invalidAddr) { + throw new BuildTransactionError(`Invalid signing address length: expected 20 bytes, got ${invalidAddr.length}`); + } + + return signingAddresses; } /** - * Compute addressesIndex from parsed transaction data. - * Similar to computeAddressesIndex() but used when parsing existing transactions - * via initBuilder(). - * - * IMPORTANT: UTXO addresses are sorted lexicographically by byte value to match - * on-chain storage order, ensuring consistency with fresh builds. + * Compute addressesIndex for UTXOs. + * addressesIndex[senderIdx] = position of sender[senderIdx] in UTXO's sorted address list. * + * UTXO addresses are sorted lexicographically by byte value to match on-chain storage order. + * @param forceRecompute - If true, recompute even if addressesIndex already exists * @protected */ - protected computeAddressesIndexFromParsed(): void { + protected computeAddressesIndex(forceRecompute = false): void { const sender = this.transaction._fromAddresses; if (!sender || sender.length === 0) return; this.transaction._utxos.forEach((utxo) => { + if (!forceRecompute && utxo.addressesIndex && utxo.addressesIndex.length > 0) { + return; + } + if (utxo.addresses && utxo.addresses.length > 0) { const sortedAddresses = utils.sortAddressesByHex(utxo.addresses); utxo.addresses = sortedAddresses; const utxoAddresses = sortedAddresses.map((a) => utils.parseAddress(a)); - utxo.addressesIndex = sender.map((senderAddr) => - utxoAddresses.findIndex((utxoAddr) => Buffer.compare(Buffer.from(utxoAddr), Buffer.from(senderAddr)) === 0) + utxo.addressesIndex = sender.map((a) => + utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0) ); } }); @@ -168,8 +186,6 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { /** * Validate UTXOs have consistent addresses. - * Note: UTXO threshold can differ from transaction threshold - each UTXO has its own - * signature requirement based on how it was created (e.g., change outputs may have threshold=1). * @protected */ protected validateUtxoAddresses(): void { @@ -180,176 +196,50 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { if (utxo.addressesIndex?.includes(-1)) { throw new BuildTransactionError('Addresses are inconsistent: ' + utxo.txid); } - if (utxo.threshold !== undefined && utxo.threshold <= 0) { - throw new BuildTransactionError('UTXO threshold must be positive: ' + utxo.txid); - } }); } /** - * Create credential with dynamic ordering based on addressesIndex from UTXO. - * Matches AVAX P behavior: signature order depends on UTXO address positions. - * - * addressesIndex[senderIdx] = utxoPosition tells us where each sender is in the UTXO. - * We create signature slots ordered by utxoPosition (smaller position = earlier slot). + * Create credential for a UTXO following AVAX P approach. + * Embed user/recovery address, leave BitGo slot empty. + * Signing order is guaranteed: user signs first (address match), BitGo signs second (empty slot). * * @param utxo - The UTXO to create credential for - * @param threshold - Number of signatures required for this specific UTXO - * @returns Credential with empty signatures ordered based on UTXO positions + * @param threshold - Number of signatures required + * @param sigIndices - Optional sigIndices from FlareJS (if not provided, derived from addressesIndex) * @protected */ - protected createCredentialForUtxo(utxo: DecodedUtxoObj, threshold: number): Credential { + protected createCredentialForUtxo(utxo: DecodedUtxoObj, threshold: number, sigIndices?: number[]): Credential { const sender = this.transaction._fromAddresses; const addressesIndex = utxo.addressesIndex ?? []; - - // either user (0) or recovery (2) const firstIndex = this.recoverSigner ? 2 : 0; const bitgoIndex = 1; if (threshold === 1) { - if (sender && sender.length > firstIndex && addressesIndex[firstIndex] !== undefined) { + if (sender && sender.length > firstIndex) { return new Credential([utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex'))]); } return new Credential([utils.createNewSig('')]); } - // If we have valid addressesIndex, use it to determine signature order - // addressesIndex[senderIdx] = position in UTXO - // Smaller position = earlier slot in signature array - if (addressesIndex.length >= 2 && sender && sender.length >= threshold) { - let emptySignatures: ReturnType[]; - - if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) { - emptySignatures = [ - utils.createNewSig(''), - utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')), - ]; - } else { - emptySignatures = [ - utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')), - utils.createNewSig(''), - ]; - } - return new Credential(emptySignatures); - } - - const emptySignatures: ReturnType[] = []; - for (let i = 0; i < threshold; i++) { - emptySignatures.push(utils.createNewSig('')); - } - return new Credential(emptySignatures); - } - - /** - * Create AddressMap based on addressesIndex following AVAX P approach. - * Maps each sender address to its signature slot based on UTXO position ordering. - * - * addressesIndex[senderIdx] = utxoPosition - * Signature slots are ordered by utxoPosition (smaller = earlier slot). - * - * @param utxo - The UTXO to create AddressMap for - * @param threshold - Number of signatures required for this specific UTXO - * @returns AddressMap that maps addresses to signature slots based on UTXO order - * @protected - */ - protected createAddressMapForUtxo(utxo: DecodedUtxoObj, threshold: number): FlareUtils.AddressMap { - const addressMap = new FlareUtils.AddressMap(); - const sender = this.transaction._fromAddresses; - const addressesIndex = utxo.addressesIndex ?? []; - - const firstIndex = this.recoverSigner ? 2 : 0; - const bitgoIndex = 1; - - if (threshold === 1) { - if (sender && sender.length > firstIndex) { - addressMap.set(new Address(sender[firstIndex]), 0); - } else if (sender && sender.length > 0) { - addressMap.set(new Address(sender[0]), 0); - } - return addressMap; - } - if (addressesIndex.length >= 2 && sender && sender.length >= threshold) { - if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) { - addressMap.set(new Address(sender[bitgoIndex]), 0); - addressMap.set(new Address(sender[firstIndex]), 1); - } else { - addressMap.set(new Address(sender[firstIndex]), 0); - addressMap.set(new Address(sender[bitgoIndex]), 1); - } - return addressMap; - } - - if (sender && sender.length >= threshold) { - sender.slice(0, threshold).forEach((addr, i) => { - addressMap.set(new Address(addr), i); - }); - } - - return addressMap; - } - - /** - * Create credential using the ACTUAL sigIndices from FlareJS. - * - * This method determines which sender addresses correspond to which sigIndex positions, - * then creates the credential with signatures in the correct order matching the sigIndices. - * - * sigIndices tell us which positions in the UTXO's owner addresses need to sign. - * We need to figure out which sender addresses are at those positions and create - * signature slots in the same order as sigIndices. - * - * @param utxo - The UTXO to create credential for - * @param threshold - Number of signatures required - * @param actualSigIndices - The actual sigIndices from FlareJS's built input - * @returns Credential with signatures ordered to match sigIndices - * @protected - */ - protected createCredentialForUtxoWithSigIndices( - utxo: DecodedUtxoObj, - threshold: number, - actualSigIndices: number[] - ): Credential { - const sender = this.transaction._fromAddresses; - const addressesIndex = utxo.addressesIndex ?? []; - - // either user (0) or recovery (2) - const firstIndex = this.recoverSigner ? 2 : 0; - - if (threshold === 1) { - if (sender && sender.length > firstIndex && addressesIndex[firstIndex] !== undefined) { - return new Credential([utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex'))]); - } - return new Credential([utils.createNewSig('')]); - } + const effectiveSigIndices = + sigIndices && sigIndices.length >= 2 + ? sigIndices + : [addressesIndex[firstIndex], addressesIndex[bitgoIndex]].sort((a, b) => a - b); - // For threshold >= 2, use the actual sigIndices order from FlareJS - // sigIndices[i] = position in UTXO's owner addresses that needs to sign - // addressesIndex[senderIdx] = position in UTXO's owner addresses for that sender - // - // We need to find which sender corresponds to each sigIndex and create signatures - // in the sigIndices order. - if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) { const emptySignatures: ReturnType[] = []; - - for (const sigIdx of actualSigIndices) { - // Find which sender address is at this UTXO position - // addressesIndex[senderIdx] tells us which UTXO position each sender is at + for (const sigIdx of effectiveSigIndices) { const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx); - if (senderIdx === firstIndex) { - // This sigIndex slot is for user/recovery - embed their address emptySignatures.push(utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex'))); } else { - // BitGo (HSM) or unknown sender - empty signature emptySignatures.push(utils.createNewSig('')); } } - return new Credential(emptySignatures); } - // Fallback: create threshold empty signatures const emptySignatures: ReturnType[] = []; for (let i = 0; i < threshold; i++) { emptySignatures.push(utils.createNewSig('')); @@ -358,25 +248,21 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { } /** - * Create AddressMap using the ACTUAL sigIndices from FlareJS. - * - * Maps sender addresses to signature slots based on the actual sigIndices order. + * Create AddressMap for a UTXO following AVAX P approach. * * @param utxo - The UTXO to create AddressMap for * @param threshold - Number of signatures required - * @param actualSigIndices - The actual sigIndices from FlareJS's built input - * @returns AddressMap that maps addresses to signature slots + * @param sigIndices - Optional sigIndices from FlareJS (if not provided, derived from addressesIndex) * @protected */ - protected createAddressMapForUtxoWithSigIndices( + protected createAddressMapForUtxo( utxo: DecodedUtxoObj, threshold: number, - actualSigIndices: number[] + sigIndices?: number[] ): FlareUtils.AddressMap { const addressMap = new FlareUtils.AddressMap(); const sender = this.transaction._fromAddresses; const addressesIndex = utxo.addressesIndex ?? []; - const firstIndex = this.recoverSigner ? 2 : 0; const bitgoIndex = 1; @@ -389,27 +275,26 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { return addressMap; } - // For threshold >= 2, map addresses based on actual sigIndices order - if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) { - actualSigIndices.forEach((sigIdx, slotIdx) => { - // Find which sender is at this UTXO position - const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx); + if (addressesIndex.length >= 2 && sender && sender.length >= threshold) { + const effectiveSigIndices = + sigIndices && sigIndices.length >= 2 + ? sigIndices + : [addressesIndex[firstIndex], addressesIndex[bitgoIndex]].sort((a, b) => a - b); + effectiveSigIndices.forEach((sigIdx, slotIdx) => { + const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx); if (senderIdx === bitgoIndex || senderIdx === firstIndex) { addressMap.set(new Address(sender[senderIdx]), slotIdx); } }); - return addressMap; } - // Fallback if (sender && sender.length >= threshold) { sender.slice(0, threshold).forEach((addr, i) => { addressMap.set(new Address(addr), i); }); } - return addressMap; } } diff --git a/modules/sdk-coin-flrp/test/resources/account.ts b/modules/sdk-coin-flrp/test/resources/account.ts index 4efa680eed..2fbed2560d 100644 --- a/modules/sdk-coin-flrp/test/resources/account.ts +++ b/modules/sdk-coin-flrp/test/resources/account.ts @@ -93,3 +93,25 @@ export const INVALID_PRIVATE_KEY_ERROR_MESSAGE = 'Unsupported private key'; export const INVALID_PUBLIC_KEY_ERROR_MESSAGE = 'Unsupported public key'; export const INVALID_LONG_KEYPAIR_PRV = SEED_ACCOUNT.privateKey + 'F1'; + +/** + * On-chain verified test wallet keys + * Used for testing transactions that have been validated on Flare Coston2 testnet + */ +export const ON_CHAIN_TEST_WALLET = { + user: { + privateKey: 'c9e1638b866930153d06d30234ab80b34cd8af52715c881cb5a3b60a7aad3fef', + pChainAddress: 'P-costwo1cgpnd2v0a0qxevncn4m7st2lychuyn7czpd5k9', + corethAddress: 'C-costwo1cgpnd2v0a0qxevncn4m7st2lychuyn7czpd5k9', + }, + bitgo: { + privateKey: '854309c49c8064a806b1d95964b35d23a3b01b4f8dd16884348f37669af6a0ed', + pChainAddress: 'P-costwo1xz2dh8g69l7almnks86l47m7dand9pzfnfpxwp', + corethAddress: 'C-costwo1xz2dh8g69l7almnks86l47m7dand9pzfnfpxwp', + }, + backup: { + privateKey: '00bd5a0bfdb5e4b536c4ccdf181014bfb8b022d49e563f48fc799b9d2f20def0', + pChainAddress: 'P-costwo107q8ch4jsa58xdjlhl7ggazu6vgqgekhuszr7a', + corethAddress: 'C-costwo107q8ch4jsa58xdjlhl7ggazu6vgqgekhuszr7a', + }, +}; diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/exportInP.ts b/modules/sdk-coin-flrp/test/resources/transactionData/exportInP.ts index ebf3a9c414..7c7d2504e6 100644 --- a/modules/sdk-coin-flrp/test/resources/transactionData/exportInP.ts +++ b/modules/sdk-coin-flrp/test/resources/transactionData/exportInP.ts @@ -1,13 +1,13 @@ // Test data for export with single UTXO export const EXPORT_IN_P = { - txhash: '2HN4sSQeyS1T8ztWdsVv4Cyj3D8zJxGTMaSZeqWVmBfJEiYsiQ', + txhash: 'yvtymyJkKtDbnLSj5wTbZWzhsreLCWtiVXNTFJ16KdQG74BqR', // Unsigned tx from script (with empty signatures + credential structure) unsignedHex: - '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650016aec5964', + '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000dacdc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000001e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000001000000020000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd7aa49f', halfSigntxHex: - '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000020000000900000001ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000900000002ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001329c4a92', + '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000dacdc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000001e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000001000000020000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000158a4a2e7e34d53b606dd8bef03549377c7133a0da47ca74d5636ed204e9c07761d0c2ada1b83956db630a583444532bb52f85ae0f21c700a748b0e06d8a7e7ab01000000090000000258a4a2e7e34d53b606dd8bef03549377c7133a0da47ca74d5636ed204e9c07761d0c2ada1b83956db630a583444532bb52f85ae0f21c700a748b0e06d8a7e7ab010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000faba85b6', fullSigntxHex: - '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000020000000900000001ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000900000002ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000fd603ba6d1c63b84c0bfb50e8a2eef0a745379fbf687a09d15f2b411907eb886d725ff52060e7863258c5abe506a690446923ec9f00b97487c2df781aea88ab0181fe5c0a', + '0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000dacdc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000001e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000001000000020000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000158a4a2e7e34d53b606dd8bef03549377c7133a0da47ca74d5636ed204e9c07761d0c2ada1b83956db630a583444532bb52f85ae0f21c700a748b0e06d8a7e7ab01000000090000000258a4a2e7e34d53b606dd8bef03549377c7133a0da47ca74d5636ed204e9c07761d0c2ada1b83956db630a583444532bb52f85ae0f21c700a748b0e06d8a7e7ab014b4dc51db37426d1011e5c4f22bd60cc253000068730a49ade743a596804a6a4457614c236f0352c2a4ad0c9fa651a97543617cb7504a367b13ae0521cb2d4ba009d204822', amount: '55000000', // 0.055 (0.05 FLR + 0.005 FLR fee) pAddresses: [ 'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu', diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts b/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts index f8e64f35c9..73128372d1 100644 --- a/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts +++ b/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts @@ -1,11 +1,11 @@ export const IMPORT_IN_C = { - txhash: '2WoCobCwRcNGN1V57HJnRjwSus7Ejx4UavtcByJQiHZdDx2CgX', + txhash: 'wHjzL4QDv3NoLA5yfAKTNLvkwT9XpnqLFE3kR5LtySYHae1nX', unsignedHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500198b8ee44', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000001000000020000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b916ed7f', halfSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf366500100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001ab8f2763', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000001000000020000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de97f4e9', fullSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000000000001ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae016fbefab7d2cdd70fdb6a83733ebbb971c97d10798b06cbb4b568a8e913c628f54f0fc1ca30ab91e5e13ff0909fc9ebe94d56787ad43fd251f83b7fedb718b01d0100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae016fbefab7d2cdd70fdb6a83733ebbb971c97d10798b06cbb4b568a8e913c628f54f0fc1ca30ab91e5e13ff0909fc9ebe94d56787ad43fd251f83b7fedb718b01d0100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae016fbefab7d2cdd70fdb6a83733ebbb971c97d10798b06cbb4b568a8e913c628f54f0fc1ca30ab91e5e13ff0909fc9ebe94d56787ad43fd251f83b7fedb718b01d0100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae016fbefab7d2cdd70fdb6a83733ebbb971c97d10798b06cbb4b568a8e913c628f54f0fc1ca30ab91e5e13ff0909fc9ebe94d56787ad43fd251f83b7fedb718b01d0100000009000000027ddd847561ba582f4e35b7ca01ccdf8961f13199b9d885c1ac79d9d852a3ec7163f82662b0544744ab3564bfd675bdd2355255dce6cf808ea5ca6e462cea99ae016fbefab7d2cdd70fdb6a83733ebbb971c97d10798b06cbb4b568a8e913c628f54f0fc1ca30ab91e5e13ff0909fc9ebe94d56787ad43fd251f83b7fedb718b01d0110e51104', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000058781ab65cabfe88b9ee1d13d61182e07a4264b3774e351de3f0ebeb8314e92700000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ace9fd62aadb8b8825eb285edd311be25e8de543959616ead19f360fabc370560000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002b9f3b7bb347a74a52688f98ab8d9c9a095420d63179d7c8030b2deb2dbe8d5500000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc0000000020000000100000002ba561801af08fa19dc3cd6767a328ed6d203ba82f27b8013220e7a26f1e3d1ac0000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000003473bc00000000200000001000000020000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b500000000105f74b258734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0043bc04507832a92fae7a4f452000315bd0af44fab4b71f72453aa06a2ba8f08934a066b09a3c9484696496b88743db40b4e62935f75324997377faaee1a448f8010000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0043bc04507832a92fae7a4f452000315bd0af44fab4b71f72453aa06a2ba8f08934a066b09a3c9484696496b88743db40b4e62935f75324997377faaee1a448f8010000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0043bc04507832a92fae7a4f452000315bd0af44fab4b71f72453aa06a2ba8f08934a066b09a3c9484696496b88743db40b4e62935f75324997377faaee1a448f8010000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0043bc04507832a92fae7a4f452000315bd0af44fab4b71f72453aa06a2ba8f08934a066b09a3c9484696496b88743db40b4e62935f75324997377faaee1a448f8010000000900000002f6af77ee2421d0022051543836bea594a2a45e620f4a23bf5755a1ca4a24dc4c15bd3642712d90faa79451cde0de8a7d02b84f053a0f4de3ea9783fa3e39ee4b0043bc04507832a92fae7a4f452000315bd0af44fab4b71f72453aa06a2ba8f08934a066b09a3c9484696496b88743db40b4e62935f75324997377faaee1a448f801aecb7b56', amount: '50000000', pAddresses: [ 'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu', diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts b/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts index 57200dbb6a..ddf344c40d 100644 --- a/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts +++ b/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts @@ -1,11 +1,11 @@ export const IMPORT_IN_P = { - txhash: '8c2JV9dBWaUXqQBgmWg6PWnRXcLvFgM5xgBaKVqC9L15csCtg', + txhash: 'jup7nnjq25rzFxRQ7eUB2biAZ5hPsgHYbKex8v9GaqMUBeBvL', unsignedHex: - '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf08000000002000000000000000100000001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001e2cd8f61', + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf0800000000200000001000000020000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000854b3057', signedHex: - '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001000000010000000900000002b55278276e7d712d6896247ddc9298600a4b4e87088842edb444149e71665fef6318b69d35baeb684e197e12ebc2b9881af36d5d5b8af08cf2aaefdcf385384600c59a868ee3007a2a3d8ab44408a2f4f19a848e4bfdffe1e25d725651d53a77de433490f92fa0337042abcea24daa978f0d0725c6c2ada11e6b45a56433f82a0b0065c41625', + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf0800000000200000001000000020000000100000009000000024ca450129158ddb3124fd91dd8324202ffce1cbbd3c65d044b084088414288890bd426ca0560d8acad51d8678544dffe11d8bbdd65e65c93ed840794e8743e7f01194dab235805bebf9a9ea33bf8c9b66b7c9f81dcab04a601780092ac4669fb5f6e430a177e20c941543953381166edeff0eaa81974e63e593c646946008e2ced0182c2b2a4', halfSigntxHex: - '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001000000010000000900000002b55278276e7d712d6896247ddc9298600a4b4e87088842edb444149e71665fef6318b69d35baeb684e197e12ebc2b9881af36d5d5b8af08cf2aaefdcf3853846000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650019c805cfd', + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b8000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000014bb60d547249da44959e410165c296bbe449f50d84b42f50950de1f3ed4214900000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf0800000000200000001000000020000000100000009000000024ca450129158ddb3124fd91dd8324202ffce1cbbd3c65d044b084088414288890bd426ca0560d8acad51d8678544dffe11d8bbdd65e65c93ed840794e8743e7f010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000471237c6', cAddressPrivateKey: 'ef576892dd582d93914a3dba3b77cc4e32e470c32f4127817345473aae719d14', cAddressPublicKey: '033ca1801f51484063f3bce093413ca06f7d91c44c3883f642eb103eda5e0eaed3', diff --git a/modules/sdk-coin-flrp/test/unit/flrp.ts b/modules/sdk-coin-flrp/test/unit/flrp.ts index fcafb62473..9a2e702349 100644 --- a/modules/sdk-coin-flrp/test/unit/flrp.ts +++ b/modules/sdk-coin-flrp/test/unit/flrp.ts @@ -216,7 +216,7 @@ describe('Flrp test cases', function () { txExplain.type.should.equal(TransactionType.Export); txExplain.fee.should.have.property('fee'); txExplain.inputs.should.be.an.Array(); - txExplain.changeAmount.should.equal('14334500'); // 0xDABA24 from transaction + txExplain.changeAmount.should.equal('14339520'); // 0xDACDC0 from transaction txExplain.changeOutputs.should.be.an.Array(); txExplain.changeOutputs[0].address.should.equal( 'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu~P-costwo1cueygd7fd37g56s49k3rshqakhp6k8u3adzt6m~P-costwo1xv5mulgpe5lt4tnx2ntnylwe79azu9vpja6lut' @@ -230,7 +230,7 @@ describe('Flrp test cases', function () { txExplain.id.should.equal(EXPORT_IN_P.txhash); txExplain.fee.should.have.property('fee'); txExplain.inputs.should.be.an.Array(); - txExplain.changeAmount.should.equal('14334500'); // 0xDABA24 from transaction + txExplain.changeAmount.should.equal('14339520'); // 0xDACDC0 from transaction txExplain.changeOutputs.should.be.an.Array(); txExplain.changeOutputs[0].address.should.equal( 'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu~P-costwo1cueygd7fd37g56s49k3rshqakhp6k8u3adzt6m~P-costwo1xv5mulgpe5lt4tnx2ntnylwe79azu9vpja6lut' diff --git a/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts index 17484c8c1f..5e699aa065 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts @@ -1,7 +1,9 @@ import assert from 'assert'; import 'should'; import { EXPORT_IN_P as testData } from '../../resources/transactionData/exportInP'; +import { ON_CHAIN_TEST_WALLET } from '../../resources/account'; import { TransactionBuilderFactory } from '../../../src/lib'; +import utils from '../../../src/lib/utils'; import { coins } from '@bitgo/statics'; import signFlowTest from './signFlowTestSuit'; import { pvmSerial, UnsignedTx, TransferOutput } from '@flarenetwork/flarejs'; @@ -149,223 +151,6 @@ describe('Flrp Export In P Tx Builder', () => { }); }); - describe('UTXO address sorting fix - addresses in non-sorted order for ExportInP', () => { - /** - * This test suite verifies the fix for the address ordering bug in ExportInP. - * - * The issue: When the API returns UTXO addresses in a different order than how they're - * stored on-chain (lexicographically sorted by byte value), the sigIndices would be - * computed incorrectly, causing signature verification to fail. - * - * The fix: Sort UTXO addresses before computing addressesIndex to match on-chain order. - */ - - // Helper to create UTXO with specific address order - const createUtxoWithAddressOrder = (utxo: (typeof testData.utxos)[0], addresses: string[]) => ({ - ...utxo, - addresses: addresses, - }); - - it('should correctly sort UTXO addresses when building ExportInP transaction', async () => { - // Create UTXOs with addresses in reversed order (simulating API returning unsorted) - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - // Should not throw - the fix ensures addresses are sorted before computing sigIndices - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(22); // Export In P type - txJson.threshold.should.equal(2); - }); - - it('should produce same transaction hex regardless of input UTXO address order for ExportInP', async () => { - // Build with original address order - const txBuilder1 = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const tx1 = await txBuilder1.build(); - const hex1 = tx1.toBroadcastFormat(); - - // Build with reversed address order in UTXOs - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder2 = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - const tx2 = await txBuilder2.build(); - const hex2 = tx2.toBroadcastFormat(); - - // Both should produce the same hex since addresses get sorted - hex1.should.equal(hex2); - }); - - it('should handle signing correctly with unsorted UTXO addresses for ExportInP', async () => { - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - // Should have signatures after signing (count depends on UTXO thresholds) - txJson.signatures.length.should.be.greaterThan(0); - tx.toBroadcastFormat().should.be.a.String(); - }); - - it('should produce valid signed transaction matching expected output with unsorted addresses for ExportInP', async () => { - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); - - const tx = await txBuilder.build(); - - // The signed tx should match the expected fullSigntxHex from testData - tx.toBroadcastFormat().should.equal(testData.fullSigntxHex); - tx.id.should.equal(testData.txhash); - }); - }); - - describe('addressesIndex extraction and signature ordering', () => { - it('should extract addressesIndex from parsed transaction inputs', async () => { - const txBuilder = factory.from(testData.halfSigntxHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(22); - txJson.signatures.length.should.be.greaterThan(0); - }); - - it('should correctly handle fresh build with proper signature ordering', async () => { - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(22); - tx.toBroadcastFormat().should.be.a.String(); - }); - - it('should correctly build and sign with different UTXO address ordering', async () => { - const reorderedUtxos = testData.utxos.map((utxo) => ({ - ...utxo, - addresses: [testData.pAddresses[1], testData.pAddresses[2], testData.pAddresses[0]], - })); - - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(reorderedUtxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(22); - tx.toBroadcastFormat().should.be.a.String(); - }); - - it('should handle parse-sign-parse-sign flow correctly', async () => { - const txBuilder = factory - .getExportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .amount(testData.amount) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - const halfSignedTx = await txBuilder.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - const txBuilder2 = factory.from(halfSignedHex); - txBuilder2.sign({ key: testData.privateKeys[0] }); - const fullSignedTx = await txBuilder2.build(); - const fullSignedJson = fullSignedTx.toJson(); - - fullSignedJson.type.should.equal(22); - fullSignedJson.signatures.length.should.be.greaterThan(0); - fullSignedTx.toBroadcastFormat().should.be.a.String(); - }); - }); - describe('Change output threshold fix', () => { /** * This test suite verifies the fix for the change output threshold bug. @@ -502,4 +287,94 @@ describe('Flrp Export In P Tx Builder', () => { }); }); }); + + describe('on-chain verified transactions', () => { + it('should build and sign export tx with correct sigIndices - on-chain verified', async () => { + const utxos = [ + { + outputID: 7, + amount: '50000000', + txid: 'bgHnEJ64td8u31aZrGDaWcDqxZ8vDV5qGd7bmSifgvUnUW8v2', + threshold: 2, + addresses: [ + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ], + outputidx: '0', + locktime: '0', + }, + { + outputID: 7, + amount: '50000000', + txid: 'KdrKz1SHM11dpDGHUthRc9sgS1hnb48pfvnmZDtJu7dRFF2Ha', + threshold: 2, + addresses: [ + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ], + outputidx: '0', + locktime: '0', + }, + ]; + + const senderPAddresses = [ + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ]; + + const exportAmount = '30000000'; + + const txBuilder = factory + .getExportInPBuilder() + .threshold(2) + .locktime(0) + .fromPubKey(senderPAddresses) + .amount(exportAmount) + .externalChainId(testData.sourceChainId) + .decodedUtxos(utxos) + .context(testData.context) + .feeState(testData.feeState); + + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.user.privateKey }); + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.bitgo.privateKey }); + + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + + tx.id.should.equal('nSBwNcgfLbk5S425b1qaYaqTTCiMCV75KU4Fbnq8SPUUqLq2'); + + const hex = rawTx.replace('0x', ''); + + const amountHex = '0000000002faf080'; + const amountPos = hex.indexOf(amountHex); + amountPos.should.be.greaterThan(0); + + const inputSection = hex.substring(amountPos + 16, amountPos + 40); + const numSigIndices = parseInt(inputSection.substring(0, 8), 16); + const sigIdx0 = parseInt(inputSection.substring(8, 16), 16); + const sigIdx1 = parseInt(inputSection.substring(16, 24), 16); + + numSigIndices.should.equal(2); + sigIdx0.should.equal(0); + sigIdx1.should.equal(2); + + const flareTransaction = (tx as any)._flareTransaction as UnsignedTx; + const innerTx = flareTransaction.getTx() as pvmSerial.ExportTx; + const changeOutputs = innerTx.baseTx.outputs; + changeOutputs.length.should.be.greaterThan(0, 'Should have change output'); + + const changeOutput = changeOutputs[0].output as TransferOutput; + changeOutput.outputOwners.threshold.value().should.equal(2); + changeOutput.outputOwners.addrs.length.should.equal(3); + + const expectedAddressBytes = senderPAddresses.map((addr) => utils.parseAddress(addr)); + const expectedAddressHexes = expectedAddressBytes.map((buf) => buf.toString('hex')).sort(); + const actualAddressHexes = changeOutput.outputOwners.addrs.map((addr) => addr.toHex().replace('0x', '')).sort(); + + actualAddressHexes.should.deepEqual(expectedAddressHexes); + }); + }); }); diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts index a9fb62dedf..710b312508 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts @@ -3,6 +3,7 @@ import 'should'; import { TransactionBuilderFactory } from '../../../src/lib'; import { coins } from '@bitgo/statics'; import { IMPORT_IN_C as testData } from '../../resources/transactionData/importInC'; +import { ON_CHAIN_TEST_WALLET } from '../../resources/account'; import signFlowTest from './signFlowTestSuit'; describe('Flrp Import In C Tx Builder', () => { @@ -153,231 +154,69 @@ describe('Flrp Import In C Tx Builder', () => { }, txHash: testData.txhash, }); - describe('addressesIndex extraction and signature slot mapping for ImportInC', () => { - it('should correctly parse half-signed ImportInC tx and add second signature', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.halfSigntxHex); - txBuilder.sign({ key: testData.privateKeys[0] }); - const tx = await txBuilder.build(); - const rawTx = tx.toBroadcastFormat(); - rawTx.should.equal(testData.fullSigntxHex); - tx.id.should.equal(testData.txhash); - }); - - it('should preserve transaction structure when parsing unsigned ImportInC tx', async () => { - const parsedBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - const parsedTx = await parsedBuilder.build(); - const parsedHex = parsedTx.toBroadcastFormat(); - parsedHex.should.equal(testData.unsignedHex); - }); - - it('should correctly handle ImportInC signing flow: parse -> sign -> parse -> sign', async () => { - // Step 1: unsigned transaction - const builder1 = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - unsignedHex.should.equal(testData.unsignedHex); - const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex); - builder2.sign({ key: testData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex); - builder3.sign({ key: testData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - fullSignedTx.toBroadcastFormat().should.equal(testData.fullSigntxHex); - fullSignedTx.id.should.equal(testData.txhash); - }); - - it('should have correct number of signatures for ImportInC after full sign flow', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.fullSigntxHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(2); - }); - - it('should have correct number of signatures for ImportInC half-signed tx', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.halfSigntxHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(1); - }); - - it('should have 0 signatures for unsigned ImportInC tx', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(0); - }); - }); - describe('UTXO address sorting fix - addresses in non-sorted order for ImportInC', () => { - /** - * This test suite verifies the fix for the address ordering bug in ImportInC. - * - * The issue: When the API returns UTXO addresses in a different order than how they're - * stored on-chain (lexicographically sorted by byte value), the sigIndices would be - * computed incorrectly, causing signature verification to fail. - * - * The fix: Sort UTXO addresses before computing addressesIndex to match on-chain order. - */ - - // Helper to create UTXO with specific address order - const createUtxoWithAddressOrder = (utxo: (typeof testData.utxos)[0], addresses: string[]) => ({ - ...utxo, - addresses: addresses, - }); - - it('should correctly sort UTXO addresses when building ImportInC transaction', async () => { - // Create UTXOs with addresses in reversed order (simulating API returning unsorted) - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); + describe('on-chain verified transactions', () => { + it('should build and sign import to C-chain tx with correct sigIndices - on-chain verified', async () => { + const utxo = { + outputID: 7, + amount: '30000000', + txid: 'nSBwNcgfLbk5S425b1qaYaqTTCiMCV75KU4Fbnq8SPUUqLq2', + threshold: 2, + addresses: [ + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ], + outputidx: '1', + locktime: '0', + }; + + const senderPAddresses = [ + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ]; + + const toAddress = '0x96993BAEb6AaE2e06BF95F144e2775D4f8efbD35'; + const importFee = '1000000'; const txBuilder = factory .getImportInCBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .to(testData.to) + .threshold(2) + .locktime(0) + .fromPubKey(senderPAddresses) + .to(toAddress) + .fee(importFee) .externalChainId(testData.sourceChainId) - .fee(testData.fee) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - // Should not throw - the fix ensures addresses are sorted before computing sigIndices - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - // Verify transaction built successfully - txJson.threshold.should.equal(2); - tx.toBroadcastFormat().should.be.a.String(); - }); - - it('should produce same transaction hex regardless of input UTXO address order for ImportInC', async () => { - // Build with original address order - const txBuilder1 = factory - .getImportInCBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .to(testData.to) - .externalChainId(testData.sourceChainId) - .fee(testData.fee) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const tx1 = await txBuilder1.build(); - const hex1 = tx1.toBroadcastFormat(); - - // Build with reversed address order in UTXOs - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder2 = factory - .getImportInCBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .to(testData.to) - .externalChainId(testData.sourceChainId) - .fee(testData.fee) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - const tx2 = await txBuilder2.build(); - const hex2 = tx2.toBroadcastFormat(); - - // Both should produce the same hex since addresses get sorted - hex1.should.equal(hex2); - }); - - it('should handle signing correctly with unsorted UTXO addresses for ImportInC', async () => { - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder = factory - .getImportInCBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .to(testData.to) - .externalChainId(testData.sourceChainId) - .fee(testData.fee) - .context(testData.context) - .decodedUtxos(reversedUtxos); - - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - // Should have 2 signatures after signing - txJson.signatures.length.should.equal(2); - }); - - it('should produce valid signed transaction matching expected output with unsorted addresses for ImportInC', async () => { - const reversedUtxos = testData.utxos.map((utxo) => - createUtxoWithAddressOrder(utxo, [...utxo.addresses].reverse()) - ); - - const txBuilder = factory - .getImportInCBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.pAddresses) - .to(testData.to) - .externalChainId(testData.sourceChainId) - .fee(testData.fee) - .context(testData.context) - .decodedUtxos(reversedUtxos); + .decodedUtxos([utxo]) + .context(testData.context); - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.user.privateKey }); + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.bitgo.privateKey }); const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); - // The signed tx should match the expected fullSigntxHex from testData - tx.toBroadcastFormat().should.equal(testData.fullSigntxHex); - tx.id.should.equal(testData.txhash); - }); - }); - - describe('fresh build with different UTXO address order for ImportInC', () => { - it('should correctly complete full sign flow with different UTXO address order for ImportInC', async () => { - const builder1 = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex); - builder2.sign({ key: testData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex); - builder3.sign({ key: testData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); + tx.id.should.equal('2t4gxEAdPLiiy9HsbjaQun1mVFewMoixNS64eZ56C38L4mpP1j'); - fullSignedTx.toJson().signatures.length.should.equal(2); + const hex = rawTx.replace('0x', ''); - const txId = fullSignedTx.id; - txId.should.be.a.String(); - txId.length.should.be.greaterThan(0); - }); + const amountHex = '0000000001c9c380'; + const amountPos = hex.indexOf(amountHex); + amountPos.should.be.greaterThan(0); - it('should handle ImportInC signing in different order and still produce valid tx', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); + const inputSection = hex.substring(amountPos + 16, amountPos + 40); + const numSigIndices = parseInt(inputSection.substring(0, 8), 16); + const sigIdx0 = parseInt(inputSection.substring(8, 16), 16); + const sigIdx1 = parseInt(inputSection.substring(16, 24), 16); - txBuilder.sign({ key: testData.privateKeys[0] }); - txBuilder.sign({ key: testData.privateKeys[2] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); + numSigIndices.should.equal(2); + sigIdx0.should.equal(0); + sigIdx1.should.equal(2); - txJson.signatures.length.should.equal(2); + const expectedOutput = parseInt(utxo.amount, 10) - parseInt(importFee, 10); + const outputHex = expectedOutput.toString(16).padStart(16, '0'); + hex.should.containEql(outputHex); }); }); }); diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts index f20730b4dc..9d7c3412f3 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import 'should'; import { IMPORT_IN_P as testData } from '../../resources/transactionData/importInP'; +import { ON_CHAIN_TEST_WALLET } from '../../resources/account'; import { TransactionBuilderFactory } from '../../../src/lib'; import { coins } from '@bitgo/statics'; import signFlowTest from './signFlowTestSuit'; @@ -150,433 +151,65 @@ describe('Flrp Import In P Tx Builder', () => { rawTx.should.equal(signedImportHex); tx.id.should.equal('2vwvuXp47dsUmqb4vkaMk7UsukrZNapKXT2ruZhVibbjMDpqr9'); }); - }); - - describe('addressesIndex extraction and signature slot mapping', () => { - it('should correctly parse half-signed tx and add second signature', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.halfSigntxHex); - txBuilder.sign({ key: testData.privateKeys[0] }); - const tx = await txBuilder.build(); - const rawTx = tx.toBroadcastFormat(); - rawTx.should.equal(testData.signedHex); - tx.id.should.equal(testData.txhash); - }); - - it('should preserve transaction structure when parsing unsigned tx', async () => { - const freshBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const freshTx = await freshBuilder.build(); - const freshHex = freshTx.toBroadcastFormat(); - const parsedBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(freshHex); - const parsedTx = await parsedBuilder.build(); - const parsedHex = parsedTx.toBroadcastFormat(); - parsedHex.should.equal(freshHex); - }); - - it('should sign parsed unsigned tx and produce same result as fresh sign', async () => { - const freshBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - freshBuilder.sign({ key: testData.privateKeys[2] }); - freshBuilder.sign({ key: testData.privateKeys[0] }); - - const freshTx = await freshBuilder.build(); - const parsedBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - parsedBuilder.sign({ key: testData.privateKeys[2] }); - parsedBuilder.sign({ key: testData.privateKeys[0] }); - const parsedTx = await parsedBuilder.build(); - parsedTx.toBroadcastFormat().should.equal(freshTx.toBroadcastFormat()); - parsedTx.id.should.equal(freshTx.id); - }); - - it('should correctly handle signing flow: build -> parse -> sign -> parse -> sign', async () => { - const builder1 = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex); - builder2.sign({ key: testData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex); - builder3.sign({ key: testData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - fullSignedTx.toBroadcastFormat().should.equal(testData.signedHex); - fullSignedTx.id.should.equal(testData.txhash); - }); - - it('should have correct number of signatures after full sign flow', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.signedHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(2); - }); - - it('should have 1 signature after half sign', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.halfSigntxHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(1); - }); - - it('should have 0 signatures for unsigned tx', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - txJson.signatures.length.should.equal(0); - }); - }); - - describe('UTXO address sorting fix - addresses in non-sorted order', () => { - /** - * This test suite verifies the fix for the address ordering bug. - * - * The issue: When the API returns UTXO addresses in a different order than how they're - * stored on-chain (lexicographically sorted by byte value), the sigIndices would be - * computed incorrectly, causing signature verification to fail. - * - * The fix: Sort UTXO addresses before computing addressesIndex to match on-chain order. - * - * We use the existing testData addresses but create UTXOs with different address orderings - * to simulate the failed transaction scenario. - */ - - // Create UTXOs with addresses in different orders to test sorting - const createUtxoWithAddressOrder = (addresses: string[]) => ({ - outputID: 7, - amount: '50000000', - txid: testData.utxos[0].txid, - threshold: 2, - addresses: addresses, - outputidx: '0', - locktime: '0', - }); - - it('should correctly sort UTXO addresses when building transaction', async () => { - // Create UTXO with addresses in reversed order (simulating API returning unsorted) - const reversedAddresses = [...testData.utxos[0].addresses].reverse(); - const utxoWithReversedAddresses = [createUtxoWithAddressOrder(reversedAddresses)]; - - const txBuilder = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(utxoWithReversedAddresses); - - // Should not throw - the fix ensures addresses are sorted before computing sigIndices - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(23); // Import type - txJson.threshold.should.equal(2); - }); - - it('should produce same transaction hex regardless of input UTXO address order', async () => { - // Build with original address order - const txBuilder1 = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const tx1 = await txBuilder1.build(); - const hex1 = tx1.toBroadcastFormat(); - - // Build with reversed address order in UTXO - const reversedAddresses = [...testData.utxos[0].addresses].reverse(); - const utxoWithReversedAddresses = [createUtxoWithAddressOrder(reversedAddresses)]; - - const txBuilder2 = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(utxoWithReversedAddresses); - - const tx2 = await txBuilder2.build(); - const hex2 = tx2.toBroadcastFormat(); - // Both should produce the same hex since addresses get sorted - hex1.should.equal(hex2); - }); - - it('should handle multiple UTXOs with different address orders', async () => { - // Create multiple UTXOs with addresses in different orders - const addresses = testData.utxos[0].addresses; - const multipleUtxos = [ - { - outputID: 7, - amount: '30000000', - txid: testData.utxos[0].txid, - threshold: 2, - addresses: [addresses[0], addresses[1], addresses[2]], // Original order - outputidx: '0', - locktime: '0', - }, - { - outputID: 7, - amount: '20000000', - txid: '2bK27hnZ8FaR33bRBs6wrb1PkjJfseZrn3nD4LckW9gCwTrmGX', - threshold: 2, - addresses: [addresses[2], addresses[0], addresses[1]], // Different order - outputidx: '0', - locktime: '0', - }, + it('should build and sign import tx with correct sigIndices - on-chain verified', async () => { + const utxo = { + outputID: 7, + amount: '51261000', + txid: '2U4mVnvLJswhngiz3mQwouTbjNUBWnebFn7dpupycQNUwyKQfu', + threshold: 2, + addresses: [ + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ], + outputidx: '0', + locktime: '0', + }; + + const senderPAddresses = [ + ON_CHAIN_TEST_WALLET.user.pChainAddress, + ON_CHAIN_TEST_WALLET.bitgo.pChainAddress, + ON_CHAIN_TEST_WALLET.backup.pChainAddress, + ]; + const senderCAddresses = [ + ON_CHAIN_TEST_WALLET.user.corethAddress, + ON_CHAIN_TEST_WALLET.bitgo.corethAddress, + ON_CHAIN_TEST_WALLET.backup.corethAddress, ]; const txBuilder = factory .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(multipleUtxos); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - // Should have 2 inputs from the 2 UTXOs - txJson.inputs.length.should.equal(2); - txJson.type.should.equal(23); - }); - - it('should produce valid transaction that can be parsed and rebuilt with unsorted addresses', async () => { - const reversedAddresses = [...testData.utxos[0].addresses].reverse(); - const utxoWithReversedAddresses = [createUtxoWithAddressOrder(reversedAddresses)]; - - const txBuilder = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(utxoWithReversedAddresses); - - const tx = await txBuilder.build(); - const txHex = tx.toBroadcastFormat(); - - // Parse the transaction - const parsedBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(txHex); - const parsedTx = await parsedBuilder.build(); - const parsedHex = parsedTx.toBroadcastFormat(); - - // Should produce identical hex - parsedHex.should.equal(txHex); - }); - - it('should handle signing correctly with unsorted UTXO addresses', async () => { - const reversedAddresses = [...testData.utxos[0].addresses].reverse(); - const utxoWithReversedAddresses = [createUtxoWithAddressOrder(reversedAddresses)]; - - const txBuilder = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(utxoWithReversedAddresses); - - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - // Should have 2 signatures after signing - txJson.signatures.length.should.equal(2); - }); - - it('should produce valid signed transaction matching original test data signing flow', async () => { - // This test verifies that with unsorted UTXO addresses, we still get the expected signed tx - const reversedAddresses = [...testData.utxos[0].addresses].reverse(); - const utxoWithReversedAddresses = [createUtxoWithAddressOrder(reversedAddresses)]; - - const txBuilder = factory - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) + .threshold(2) + .locktime(0) + .fromPubKey(senderCAddresses) + .to(senderPAddresses) .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) + .decodedUtxos([utxo]) .context(testData.context) - .decodedUtxos(utxoWithReversedAddresses); - - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); - - const tx = await txBuilder.build(); - - // The signed tx should match the expected signedHex from testData - tx.toBroadcastFormat().should.equal(testData.signedHex); - tx.id.should.equal(testData.txhash); - }); - }); - - describe('fresh build with different UTXO address order', () => { - it('should correctly set up addressMaps when UTXO addresses differ from fromAddresses order', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(23); - txJson.threshold.should.equal(2); - }); - - it('should produce correct signatures when signing fresh build with different address order', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); + .feeState(testData.feeState); - txBuilder.sign({ key: testData.privateKeys[2] }); - txBuilder.sign({ key: testData.privateKeys[0] }); + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.user.privateKey }); + txBuilder.sign({ key: ON_CHAIN_TEST_WALLET.bitgo.privateKey }); const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - }); - - it('should produce matching tx when fresh build is parsed and rebuilt', async () => { - const freshBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const freshTx = await freshBuilder.build(); - const freshHex = freshTx.toBroadcastFormat(); - - const parsedBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(freshHex); - const parsedTx = await parsedBuilder.build(); - const parsedHex = parsedTx.toBroadcastFormat(); - - parsedHex.should.equal(freshHex); - }); - - it('should correctly complete full sign flow with different UTXO address order', async () => { - const builder1 = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex); - builder2.sign({ key: testData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex); - builder3.sign({ key: testData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); + const rawTx = tx.toBroadcastFormat(); - fullSignedTx.toJson().signatures.length.should.equal(2); + tx.id.should.equal('bgHnEJ64td8u31aZrGDaWcDqxZ8vDV5qGd7bmSifgvUnUW8v2'); - const txId = fullSignedTx.id; - txId.should.be.a.String(); - txId.length.should.be.greaterThan(0); - }); + const hex = rawTx.replace('0x', ''); + const amountHex = parseInt(utxo.amount, 10).toString(16).padStart(16, '0'); + const amountPos = hex.indexOf(amountHex); + amountPos.should.be.greaterThan(0); - it('should handle signing in different order and still produce valid tx', async () => { - const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')) - .getImportInPBuilder() - .threshold(testData.threshold) - .locktime(testData.locktime) - .fromPubKey(testData.corethAddresses) - .to(testData.pAddresses) - .externalChainId(testData.sourceChainId) - .feeState(testData.feeState) - .context(testData.context) - .decodedUtxos(testData.utxos); - - txBuilder.sign({ key: testData.privateKeys[0] }); - txBuilder.sign({ key: testData.privateKeys[2] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); + const inputSection = hex.substring(amountPos + 16, amountPos + 40); + const numSigIndices = parseInt(inputSection.substring(0, 8), 16); + const sigIdx0 = parseInt(inputSection.substring(8, 16), 16); + const sigIdx1 = parseInt(inputSection.substring(16, 24), 16); - txJson.signatures.length.should.equal(2); + numSigIndices.should.equal(2); + sigIdx0.should.equal(0); + sigIdx1.should.equal(2); }); }); }); diff --git a/modules/sdk-coin-flrp/test/unit/lib/signatureIndex.ts b/modules/sdk-coin-flrp/test/unit/lib/signatureIndex.ts deleted file mode 100644 index 417b757555..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/signatureIndex.ts +++ /dev/null @@ -1,1377 +0,0 @@ -/** - * Comprehensive test suite for signature index handling in FLRP atomic transactions. - * - * This tests the alignment with AVAX P implementation for: - * 1. addressesIndex computation (mapping sender addresses to UTXO positions) - * 2. Signature slot ordering (based on UTXO address positions) - * 3. Two-phase signing flow (build -> parse -> sign -> parse -> sign) - * 4. Different UTXO address orderings - * 5. Mixed threshold UTXOs (threshold=1 and threshold=2) - * 6. Recovery signing scenarios - */ -import 'should'; -import { TransactionBuilderFactory } from '../../../src/lib'; -import { coins } from '@bitgo/statics'; -import { IMPORT_IN_P as importPTestData } from '../../resources/transactionData/importInP'; -import { IMPORT_IN_C as importCTestData } from '../../resources/transactionData/importInC'; -import { EXPORT_IN_P as exportPTestData } from '../../resources/transactionData/exportInP'; - -describe('Signature Index Handling - AVAX P Alignment', () => { - const coinConfig = coins.get('tflrp'); - const newFactory = () => new TransactionBuilderFactory(coinConfig); - - describe('addressesIndex Computation', () => { - describe('Import In P - addressesIndex scenarios', () => { - it('should compute addressesIndex correctly when UTXO addresses differ from sender order', async () => { - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(23); - txJson.threshold.should.equal(2); - }); - - it('should handle UTXO where addresses match sender order exactly', async () => { - const utxosMatchingOrder = [ - { - ...importPTestData.utxos[0], - addresses: [importPTestData.pAddresses[0], importPTestData.pAddresses[1], importPTestData.pAddresses[2]], - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(utxosMatchingOrder); - - txBuilder.sign({ key: importPTestData.privateKeys[2] }); - txBuilder.sign({ key: importPTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - }); - - it('should handle UTXO where bitgo address comes before user in UTXO list', async () => { - const utxosBitgoFirst = [ - { - ...importPTestData.utxos[0], - addresses: [importPTestData.pAddresses[1], importPTestData.pAddresses[0], importPTestData.pAddresses[2]], - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(utxosBitgoFirst); - - txBuilder.sign({ key: importPTestData.privateKeys[2] }); - txBuilder.sign({ key: importPTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - tx.toBroadcastFormat().should.be.a.String(); - }); - - it('should handle UTXO where user address comes before bitgo in UTXO list', async () => { - const utxosUserFirst = [ - { - ...importPTestData.utxos[0], - addresses: [importPTestData.pAddresses[0], importPTestData.pAddresses[1], importPTestData.pAddresses[2]], - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(utxosUserFirst); - - txBuilder.sign({ key: importPTestData.privateKeys[2] }); - txBuilder.sign({ key: importPTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - tx.toBroadcastFormat().should.be.a.String(); - }); - }); - - describe('Import In C - addressesIndex scenarios', () => { - it('should compute addressesIndex correctly for C-chain import with multiple UTXOs', async () => { - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(importCTestData.utxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(23); - }); - - it('should handle multiple UTXOs with same address ordering', async () => { - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(importCTestData.utxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - }); - }); - - describe('Export In P - addressesIndex scenarios with mixed thresholds', () => { - it('should handle UTXOs with different thresholds (threshold=1 and threshold=2)', async () => { - const txBuilder = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount(exportPTestData.amount) - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(exportPTestData.utxos); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.type.should.equal(22); - }); - - it('should correctly sign threshold=1 UTXOs with single signature slot', async () => { - const threshold1Utxos = exportPTestData.utxos - .filter((u) => u.threshold === 1) - .map((u) => ({ - ...u, - amount: (BigInt(u.amount) * 2n).toString(), - })); - - if (threshold1Utxos.length > 0) { - const txBuilder = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount('20000000') - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(threshold1Utxos); - - txBuilder.sign({ key: exportPTestData.privateKeys[2] }); - - const tx = await txBuilder.build(); - tx.toBroadcastFormat().should.be.a.String(); - } - }); - }); - }); - - describe('Two-Phase Signing Flow', () => { - describe('Import In P - Two-phase signing', () => { - it('should correctly preserve addressesIndex through parse-sign-parse-sign flow', async () => { - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - unsignedTx.toJson().signatures.length.should.equal(0); - - const builder2 = newFactory().from(unsignedHex); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder3 = newFactory().from(halfSignedHex); - builder3.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - fullSignedTx.toJson().signatures.length.should.equal(2); - - fullSignedTx.toBroadcastFormat().should.equal(importPTestData.signedHex); - fullSignedTx.id.should.equal(importPTestData.txhash); - }); - - it('should produce same result when signing in single phase vs two phases', async () => { - const singlePhaseBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - singlePhaseBuilder.sign({ key: importPTestData.privateKeys[2] }); - singlePhaseBuilder.sign({ key: importPTestData.privateKeys[0] }); - const singlePhaseTx = await singlePhaseBuilder.build(); - - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const unsignedTx = await builder1.build(); - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: importPTestData.privateKeys[0] }); - const twoPhasesTx = await builder3.build(); - - singlePhaseTx.toBroadcastFormat().should.equal(twoPhasesTx.toBroadcastFormat()); - singlePhaseTx.id.should.equal(twoPhasesTx.id); - }); - - it('should handle signing from parsed unsigned tx directly', async () => { - const builder = newFactory().from(importPTestData.unsignedHex); - builder.sign({ key: importPTestData.privateKeys[2] }); - builder.sign({ key: importPTestData.privateKeys[0] }); - const tx = await builder.build(); - - tx.toBroadcastFormat().should.equal(importPTestData.signedHex); - tx.id.should.equal(importPTestData.txhash); - }); - - it('should handle signing from parsed half-signed tx', async () => { - const builder = newFactory().from(importPTestData.halfSigntxHex); - builder.sign({ key: importPTestData.privateKeys[0] }); - const tx = await builder.build(); - - tx.toBroadcastFormat().should.equal(importPTestData.signedHex); - tx.id.should.equal(importPTestData.txhash); - }); - }); - - describe('Import In C - Two-phase signing', () => { - it('should correctly handle two-phase signing for C-chain import', async () => { - const builder1 = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(importCTestData.utxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - const builder2 = newFactory().from(unsignedHex); - builder2.sign({ key: importCTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - const builder3 = newFactory().from(halfSignedHex); - builder3.sign({ key: importCTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toBroadcastFormat().should.equal(importCTestData.fullSigntxHex); - fullSignedTx.id.should.equal(importCTestData.txhash); - }); - - it('should handle multiple UTXOs in two-phase signing', async () => { - const builder1 = newFactory().from(importCTestData.unsignedHex); - const unsignedTx = await builder1.build(); - unsignedTx.toJson().signatures.length.should.equal(0); - - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: importCTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: importCTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - fullSignedTx.toJson().signatures.length.should.equal(2); - }); - }); - - describe('Export In P - Two-phase signing with mixed thresholds', () => { - it('should correctly handle two-phase signing with threshold=1 and threshold=2 UTXOs', async () => { - const builder1 = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount(exportPTestData.amount) - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(exportPTestData.utxos); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - const builder2 = newFactory().from(unsignedHex); - builder2.sign({ key: exportPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - const builder3 = newFactory().from(halfSignedHex); - builder3.sign({ key: exportPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toBroadcastFormat().should.equal(exportPTestData.fullSigntxHex); - fullSignedTx.id.should.equal(exportPTestData.txhash); - }); - }); - }); - - describe('Signature Slot Ordering', () => { - describe('Credential creation with embedded addresses', () => { - it('should embed correct address in unsigned tx signature slots', async () => { - const builder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const tx = await builder.build(); - const hex = tx.toBroadcastFormat(); - - hex.should.equal(importPTestData.unsignedHex); - }); - - it('should replace embedded address with actual signature after signing', async () => { - const builder1 = newFactory().from(importPTestData.unsignedHex); - builder1.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder1.build(); - - halfSignedTx.toBroadcastFormat().should.equal(importPTestData.halfSigntxHex); - - const builder2 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder2.build(); - - fullSignedTx.toBroadcastFormat().should.equal(importPTestData.signedHex); - }); - }); - - describe('Signing order independence', () => { - it('should produce valid signatures regardless of which key signs first (fresh build)', async () => { - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - builder1.sign({ key: importPTestData.privateKeys[2] }); - builder1.sign({ key: importPTestData.privateKeys[0] }); - const tx1 = await builder1.build(); - - const builder2 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - builder2.sign({ key: importPTestData.privateKeys[0] }); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const tx2 = await builder2.build(); - - tx1.toJson().signatures.length.should.equal(2); - tx2.toJson().signatures.length.should.equal(2); - - tx1.id.should.be.a.String(); - tx1.id.length.should.be.greaterThan(0); - tx2.id.should.be.a.String(); - tx2.id.length.should.be.greaterThan(0); - }); - - it('should produce identical tx when signing in expected slot order', async () => { - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - builder1.sign({ key: importPTestData.privateKeys[2] }); - builder1.sign({ key: importPTestData.privateKeys[0] }); - const tx1 = await builder1.build(); - - const builder2 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - builder2.sign({ key: importPTestData.privateKeys[2] }); - builder2.sign({ key: importPTestData.privateKeys[0] }); - const tx2 = await builder2.build(); - - tx1.toBroadcastFormat().should.equal(tx2.toBroadcastFormat()); - tx1.id.should.equal(tx2.id); - }); - }); - }); - - describe('Edge Cases', () => { - describe('Transaction recovery and rebuild', () => { - it('should preserve transaction structure when parsing and rebuilding unsigned tx', async () => { - const builder = newFactory().from(importPTestData.unsignedHex); - const tx = await builder.build(); - tx.toBroadcastFormat().should.equal(importPTestData.unsignedHex); - }); - - it('should preserve transaction structure when parsing and rebuilding half-signed tx', async () => { - const builder = newFactory().from(importPTestData.halfSigntxHex); - const tx = await builder.build(); - tx.toBroadcastFormat().should.equal(importPTestData.halfSigntxHex); - }); - - it('should preserve transaction structure when parsing and rebuilding fully-signed tx', async () => { - const builder = newFactory().from(importPTestData.signedHex); - const tx = await builder.build(); - tx.toBroadcastFormat().should.equal(importPTestData.signedHex); - tx.id.should.equal(importPTestData.txhash); - }); - }); - - describe('Signature count validation', () => { - it('should have 0 signatures for unsigned tx', async () => { - const builder = newFactory().from(importPTestData.unsignedHex); - const tx = await builder.build(); - tx.toJson().signatures.length.should.equal(0); - }); - - it('should have 1 signature for half-signed tx', async () => { - const builder = newFactory().from(importPTestData.halfSigntxHex); - const tx = await builder.build(); - tx.toJson().signatures.length.should.equal(1); - }); - - it('should have 2 signatures for fully-signed tx', async () => { - const builder = newFactory().from(importPTestData.signedHex); - const tx = await builder.build(); - tx.toJson().signatures.length.should.equal(2); - }); - }); - - describe('Multiple UTXOs with different address orderings', () => { - it('should handle multiple UTXOs where each has different address order', async () => { - const mixedOrderUtxos = [ - { - ...importCTestData.utxos[0], - addresses: [importCTestData.pAddresses[2], importCTestData.pAddresses[0], importCTestData.pAddresses[1]], - }, - { - ...importCTestData.utxos[1], - addresses: [importCTestData.pAddresses[0], importCTestData.pAddresses[1], importCTestData.pAddresses[2]], - }, - { - ...importCTestData.utxos[2], - addresses: [importCTestData.pAddresses[1], importCTestData.pAddresses[2], importCTestData.pAddresses[0]], - }, - ]; - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(mixedOrderUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - tx.toBroadcastFormat().should.be.a.String(); - }); - }); - - describe('Cross-builder consistency', () => { - it('should use same addressesIndex logic across all atomic builders', async () => { - const utxoAddresses = [ - importPTestData.pAddresses[2], - importPTestData.pAddresses[0], - importPTestData.pAddresses[1], - ]; - - // ImportInP - const importPBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos([{ ...importPTestData.utxos[0], addresses: utxoAddresses }]); - - importPBuilder.sign({ key: importPTestData.privateKeys[2] }); - const importPTx = await importPBuilder.build(); - importPTx.toJson().signatures.length.should.equal(1); - - const importCBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos([{ ...importCTestData.utxos[0], addresses: utxoAddresses }]) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - importCBuilder.sign({ key: importCTestData.privateKeys[2] }); - const importCTx = await importCBuilder.build(); - importCTx.toJson().signatures.length.should.equal(1); - - const exportPBuilder = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount('20000000') - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos([{ ...exportPTestData.utxos[1], addresses: utxoAddresses }]); - - exportPBuilder.sign({ key: exportPTestData.privateKeys[2] }); - const exportPTx = await exportPBuilder.build(); - exportPTx.toJson().signatures.length.should.equal(1); - }); - }); - }); - - describe('Real-world Scenario: Failed Transaction Fix Verification', () => { - it('should correctly sign when UTXO addresses are in IMS order (not BitGo order)', async () => { - const imsOrderUtxos = [ - { - ...importPTestData.utxos[0], - addresses: [importPTestData.pAddresses[1], importPTestData.pAddresses[2], importPTestData.pAddresses[0]], - }, - ]; - - const wpBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(imsOrderUtxos); - - const unsignedTx = await wpBuilder.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - const ovc1Builder = newFactory().from(unsignedHex); - ovc1Builder.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await ovc1Builder.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - halfSignedTx.toJson().signatures.length.should.equal(1); - - const ovc2Builder = newFactory().from(halfSignedHex); - ovc2Builder.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await ovc2Builder.build(); - - fullSignedTx.toJson().signatures.length.should.equal(2); - - const txId = fullSignedTx.id; - txId.should.be.a.String(); - txId.length.should.be.greaterThan(0); - }); - - it('should handle the exact UTXO configuration from the failed production transaction', async () => { - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const unsignedTx = await builder1.build(); - - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toBroadcastFormat().should.equal(importPTestData.signedHex); - fullSignedTx.id.should.equal(importPTestData.txhash); - }); - }); - - /** - * Test suite for UTXO reordering fix. - * - * FlareJS's newImportTx/newExportTx functions sort inputs by UTXO ID (txid + outputidx) - * for deterministic transaction building. The SDK must match inputs back to UTXOs - * by UTXO ID, not by array index, to ensure credentials are created for the correct inputs. - * - * These tests verify that transactions with multiple UTXOs work correctly regardless - * of the order in which UTXOs are provided. - */ - describe('UTXO Reordering Fix - Multiple UTXOs with Different txids', () => { - describe('ImportInC with reordered UTXOs', () => { - it('should correctly handle multiple UTXOs that may get reordered by FlareJS', async () => { - const reorderedUtxos = [importCTestData.utxos[4], importCTestData.utxos[0]]; - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(reorderedUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - const txJson = tx.toJson(); - - txJson.signatures.length.should.equal(2); - tx.toBroadcastFormat().should.be.a.String(); - txJson.inputs.length.should.equal(2); - }); - - it('should correctly sign in parse-sign-parse-sign flow with multiple UTXOs', async () => { - const reorderedUtxos = [importCTestData.utxos[3], importCTestData.utxos[1]]; - - const builder1 = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(reorderedUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - const unsignedTx = await builder1.build(); - unsignedTx.toJson().signatures.length.should.equal(0); - - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: importCTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: importCTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - fullSignedTx.toJson().signatures.length.should.equal(2); - - fullSignedTx.toBroadcastFormat().should.be.a.String(); - fullSignedTx.id.should.be.a.String(); - }); - - it('should handle 3+ UTXOs with different ordering', async () => { - const mixedUtxos = [importCTestData.utxos[2], importCTestData.utxos[4], importCTestData.utxos[0]]; - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(mixedUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - tx.toJson().inputs.length.should.equal(3); - }); - - it('should handle all 5 UTXOs from test data', async () => { - const allUtxosReversed = [...importCTestData.utxos].reverse(); - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(allUtxosReversed) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - tx.toJson().inputs.length.should.equal(5); - }); - }); - - describe('ImportInP with multiple UTXOs', () => { - it('should correctly handle multiple UTXOs with different outputidx', async () => { - const multipleUtxos = [ - { - ...importPTestData.utxos[0], - outputidx: '1', - amount: '25000000', - }, - { - ...importPTestData.utxos[0], - outputidx: '0', - amount: '25000000', - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(multipleUtxos); - - txBuilder.sign({ key: importPTestData.privateKeys[2] }); - txBuilder.sign({ key: importPTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - tx.toJson().inputs.length.should.equal(2); - }); - - it('should correctly sign in parse-sign-parse-sign flow with multiple UTXOs', async () => { - const multipleUtxos = [ - { - ...importPTestData.utxos[0], - outputidx: '1', - amount: '25000000', - }, - { - ...importPTestData.utxos[0], - outputidx: '0', - amount: '25000000', - }, - ]; - - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(multipleUtxos); - - const unsignedTx = await builder1.build(); - - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toJson().signatures.length.should.equal(2); - fullSignedTx.toBroadcastFormat().should.be.a.String(); - }); - }); - - describe('ExportInP with multiple UTXOs', () => { - it('should correctly handle multiple UTXOs that may get reordered', async () => { - const reorderedUtxos = [ - { - ...exportPTestData.utxos[1], - outputidx: '1', - }, - { - ...exportPTestData.utxos[1], - outputidx: '0', - }, - ]; - - const txBuilder = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount('20000000') - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(reorderedUtxos); - - txBuilder.sign({ key: exportPTestData.privateKeys[2] }); - txBuilder.sign({ key: exportPTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - }); - - it('should correctly sign in parse-sign-parse-sign flow with multiple UTXOs', async () => { - const reorderedUtxos = [ - { - ...exportPTestData.utxos[1], - outputidx: '1', - }, - { - ...exportPTestData.utxos[1], - outputidx: '0', - }, - ]; - - const builder1 = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .amount('20000000') - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(reorderedUtxos); - - const unsignedTx = await builder1.build(); - - const builder2 = newFactory().from(unsignedTx.toBroadcastFormat()); - builder2.sign({ key: exportPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - - const builder3 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder3.sign({ key: exportPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toJson().signatures.length.should.equal(2); - fullSignedTx.toBroadcastFormat().should.be.a.String(); - }); - }); - - describe('Edge cases for UTXO matching', () => { - it('should match UTXOs by both txid AND outputidx', async () => { - const sameIdUtxos = [ - { - ...importCTestData.utxos[0], - outputidx: '2', - }, - { - ...importCTestData.utxos[0], - outputidx: '0', - }, - ]; - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(sameIdUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - tx.toJson().inputs.length.should.equal(2); - }); - - it('should work correctly when UTXOs are already in sorted order', async () => { - const sortedUtxos = [importCTestData.utxos[0], importCTestData.utxos[1]]; - - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(sortedUtxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - txBuilder.sign({ key: importCTestData.privateKeys[2] }); - txBuilder.sign({ key: importCTestData.privateKeys[0] }); - - const tx = await txBuilder.build(); - tx.toJson().signatures.length.should.equal(2); - }); - }); - }); - - /** - * Tests specifically for the sigIndices fix. - * - * Background: FLRP import to P transactions were failing with: - * - "could not parse transaction" (HSM error) - * - "wrong signature" (on-chain error) - * - * Root cause: Mismatch between SDK's assumed credential order and FlareJS's - * actual sigIndices order. When UTXO addresses are sorted differently than - * sender addresses, the credentials must follow FlareJS's sigIndices order - * (ascending by UTXO position), not the SDK's sender address order. - * - * The fix reads actual sigIndices from FlareJS-built transaction inputs - * and creates credentials in that exact order. - * - * Real-world failure examples: - * 1. Wallet 697738a870cd159b6ab80f7071e3146a - HSM error "could not parse transaction" - * - UTXO addresses: [r6gzm4acz..., x3u4xeal7..., l4f9rrc7w...] - * - Sender addresses: [l4f9rrc7w..., x3u4xeal7..., r6gzm4acz...] - * - * 2. Wallet 69773884a565c4dbaf680e360b920c9e - On-chain "wrong signature" - * - UTXO addresses: [t37m2e8fa..., urz6r3jy2..., 7r4k0nqne...] - * - Sender addresses: [urz6r3jy2..., 7r4k0nqne..., t37m2e8fa...] - */ - describe('SigIndices Fix Verification', () => { - describe('Credential ordering matches FlareJS sigIndices', () => { - it('should create credentials in correct order when UTXO addresses differ from sender order', async () => { - // Test data where UTXO addresses are in different order than pAddresses - // UTXO addresses: [xv5mulgpe..., 06gc5h5qs..., cueygd7fd...] - // pAddresses: [06gc5h5qs..., cueygd7fd..., xv5mulgpe...] - // - // FlareJS will compute sigIndices based on which addresses are signing - // and their positions in the UTXO's address list (sorted by address bytes) - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const tx = await txBuilder.build(); - const hex = tx.toBroadcastFormat(); - - // The fix ensures the unsigned tx has credentials ordered correctly - // Before fix: credentials were in sender address order - // After fix: credentials are in sigIndices order (ascending UTXO position) - hex.should.equal(importPTestData.unsignedHex); - - // Verify we can sign and the signatures go in the correct slots - const builder1 = newFactory().from(hex); - builder1.sign({ key: importPTestData.privateKeys[2] }); // user key - const halfSignedTx = await builder1.build(); - - halfSignedTx.toBroadcastFormat().should.equal(importPTestData.halfSigntxHex); - - const builder2 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[0] }); // bitgo key - const fullSignedTx = await builder2.build(); - - fullSignedTx.toBroadcastFormat().should.equal(importPTestData.signedHex); - fullSignedTx.toJson().signatures.length.should.equal(2); - }); - - it('should produce valid signatures when signing keys are provided in any order', async () => { - // This tests that the fix properly handles the address mapping - // regardless of which key signs first - const txBuilder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - // Sign user first, then bitgo - txBuilder1.sign({ key: importPTestData.privateKeys[2] }); - txBuilder1.sign({ key: importPTestData.privateKeys[0] }); - const tx1 = await txBuilder1.build(); - - const txBuilder2 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - // Sign bitgo first, then user - txBuilder2.sign({ key: importPTestData.privateKeys[0] }); - txBuilder2.sign({ key: importPTestData.privateKeys[2] }); - const tx2 = await txBuilder2.build(); - - // Both should produce the same valid transaction - tx1.toBroadcastFormat().should.equal(tx2.toBroadcastFormat()); - tx1.toJson().signatures.length.should.equal(2); - tx2.toJson().signatures.length.should.equal(2); - }); - - it('should handle Export P transaction with correct sigIndices order', async () => { - const txBuilder = newFactory() - .getExportInPBuilder() - .threshold(exportPTestData.threshold) - .locktime(exportPTestData.locktime) - .fromPubKey(exportPTestData.pAddresses) - .externalChainId(exportPTestData.sourceChainId) - .feeState(exportPTestData.feeState) - .context(exportPTestData.context) - .decodedUtxos(exportPTestData.utxos) - .amount(exportPTestData.amount); - - const tx = await txBuilder.build(); - const hex = tx.toBroadcastFormat(); - - hex.should.equal(exportPTestData.unsignedHex); - - // Verify signing flow - const builder1 = newFactory().from(hex); - builder1.sign({ key: exportPTestData.privateKeys[2] }); - const halfSignedTx = await builder1.build(); - - halfSignedTx.toBroadcastFormat().should.equal(exportPTestData.halfSigntxHex); - }); - - it('should handle Import C transaction with correct sigIndices order', async () => { - const txBuilder = newFactory() - .getImportInCBuilder() - .threshold(importCTestData.threshold) - .fromPubKey(importCTestData.pAddresses) - .decodedUtxos(importCTestData.utxos) - .to(importCTestData.to) - .fee(importCTestData.fee) - .context(importCTestData.context); - - const tx = await txBuilder.build(); - const hex = tx.toBroadcastFormat(); - - hex.should.equal(importCTestData.unsignedHex); - - // Verify signing flow - const builder1 = newFactory().from(hex); - builder1.sign({ key: importCTestData.privateKeys[2] }); - const halfSignedTx = await builder1.build(); - - halfSignedTx.toBroadcastFormat().should.equal(importCTestData.halfSigntxHex); - }); - }); - - describe('Two-phase signing flow (mimics HSM flow)', () => { - it('should support build -> serialize -> parse -> sign -> serialize -> parse -> sign flow for Import P', async () => { - // Phase 1: Build unsigned transaction - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(importPTestData.utxos); - - const unsignedTx = await builder1.build(); - const unsignedHex = unsignedTx.toBroadcastFormat(); - - // Phase 2: Parse unsigned and add first signature (user/backup) - const builder2 = newFactory().from(unsignedHex); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder2.build(); - const halfSignedHex = halfSignedTx.toBroadcastFormat(); - - halfSignedTx.toJson().signatures.length.should.equal(1); - - // Phase 3: Parse half-signed and add second signature (bitgo) - const builder3 = newFactory().from(halfSignedHex); - builder3.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder3.build(); - - fullSignedTx.toJson().signatures.length.should.equal(2); - fullSignedTx.toBroadcastFormat().should.equal(importPTestData.signedHex); - }); - }); - - describe('Failed transaction scenarios (real-world cases)', () => { - /** - * This test simulates the exact failure pattern from wallet 697738a870cd159b6ab80f7071e3146a - * which failed at HSM with "could not parse transaction". - * - * The failure occurred because: - * - UTXO addresses were in order: [addr0, addr1, addr2] - * - Sender addresses were in order: [addr2, addr1, addr0] (reversed) - * - FlareJS computed sigIndices based on UTXO positions - * - SDK was creating credentials based on sender positions (wrong) - * - * After the fix, credentials are created in the order specified by FlareJS sigIndices. - */ - it('should handle address ordering where UTXO order differs significantly from sender order', async () => { - // Simulate the failed case pattern: - // UTXO addresses: [pAddresses[2], pAddresses[1], pAddresses[0]] - // Sender addresses: [pAddresses[0], pAddresses[1], pAddresses[2]] - const reorderedUtxos = [ - { - ...importPTestData.utxos[0], - addresses: [ - importPTestData.pAddresses[2], // backup at UTXO position 0 - importPTestData.pAddresses[1], // bitgo at UTXO position 1 - importPTestData.pAddresses[0], // user at UTXO position 2 - ], - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(reorderedUtxos); - - const tx = await txBuilder.build(); - const hex = tx.toBroadcastFormat(); - - // Verify transaction can be built without errors - hex.should.be.a.String(); - hex.length.should.be.greaterThan(0); - - // Verify we can parse and sign the transaction - const builder1 = newFactory().from(hex); - builder1.sign({ key: importPTestData.privateKeys[2] }); // user key - const halfSignedTx = await builder1.build(); - - halfSignedTx.toJson().signatures.length.should.equal(1); - - const builder2 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[0] }); // bitgo key - const fullSignedTx = await builder2.build(); - - fullSignedTx.toJson().signatures.length.should.equal(2); - - // Both signatures should be present and valid - const signatures = fullSignedTx.toJson().signatures; - signatures.forEach((sig: string) => { - sig.should.be.a.String(); - sig.length.should.be.greaterThan(0); - }); - }); - - /** - * This test simulates scenarios with multiple UTXOs where each has different - * address orderings, similar to the failed transaction with 4 UTXOs. - */ - it('should handle multiple UTXOs with varying address orderings', async () => { - // Create multiple UTXOs with different address orderings - // Use same txid but different outputidx to create valid unique UTXOs - const multipleUtxos = [ - { - ...importPTestData.utxos[0], - outputidx: '0', - addresses: [ - importPTestData.pAddresses[2], // backup first - importPTestData.pAddresses[0], // user second - importPTestData.pAddresses[1], // bitgo third - ], - }, - { - ...importPTestData.utxos[0], - outputidx: '1', - addresses: [ - importPTestData.pAddresses[1], // bitgo first - importPTestData.pAddresses[2], // backup second - importPTestData.pAddresses[0], // user third - ], - }, - { - ...importPTestData.utxos[0], - outputidx: '2', - addresses: [ - importPTestData.pAddresses[0], // user first - importPTestData.pAddresses[1], // bitgo second - importPTestData.pAddresses[2], // backup third - ], - }, - ]; - - const txBuilder = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(multipleUtxos); - - const tx = await txBuilder.build(); - const hex = tx.toBroadcastFormat(); - - // Verify transaction builds successfully - hex.should.be.a.String(); - tx.toJson().inputs.length.should.equal(3); - - // Verify full signing flow works - const builder1 = newFactory().from(hex); - builder1.sign({ key: importPTestData.privateKeys[2] }); - const halfSignedTx = await builder1.build(); - - const builder2 = newFactory().from(halfSignedTx.toBroadcastFormat()); - builder2.sign({ key: importPTestData.privateKeys[0] }); - const fullSignedTx = await builder2.build(); - - // Each input should have its own credential - fullSignedTx.toJson().signatures.length.should.equal(2); - }); - - /** - * This test verifies that signing order doesn't matter even with - * complex address orderings - the fix ensures signatures go to - * the correct slots based on FlareJS sigIndices. - */ - it('should produce identical results regardless of signing order with reordered addresses', async () => { - const reorderedUtxos = [ - { - ...importPTestData.utxos[0], - addresses: [importPTestData.pAddresses[2], importPTestData.pAddresses[1], importPTestData.pAddresses[0]], - }, - ]; - - // Sign user first, then bitgo - const builder1 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(reorderedUtxos); - - builder1.sign({ key: importPTestData.privateKeys[2] }); - builder1.sign({ key: importPTestData.privateKeys[0] }); - const tx1 = await builder1.build(); - - // Sign bitgo first, then user - const builder2 = newFactory() - .getImportInPBuilder() - .threshold(importPTestData.threshold) - .locktime(importPTestData.locktime) - .fromPubKey(importPTestData.corethAddresses) - .to(importPTestData.pAddresses) - .externalChainId(importPTestData.sourceChainId) - .feeState(importPTestData.feeState) - .context(importPTestData.context) - .decodedUtxos(reorderedUtxos); - - builder2.sign({ key: importPTestData.privateKeys[0] }); - builder2.sign({ key: importPTestData.privateKeys[2] }); - const tx2 = await builder2.build(); - - // Both should produce identical fully signed transactions - tx1.toBroadcastFormat().should.equal(tx2.toBroadcastFormat()); - tx1.toJson().signatures.length.should.equal(2); - tx2.toJson().signatures.length.should.equal(2); - }); - }); - }); -});