diff --git a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts index 62ef2e771e..7591277208 100644 --- a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts @@ -11,7 +11,6 @@ import { TransferableInput, Address, utils as FlareUtils, - avmSerial, } from '@flarenetwork/flarejs'; import utils from './utils'; import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface'; @@ -63,16 +62,6 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { // Calculate fee based on input/output difference const fee = totalInputAmount - totalOutputAmount; - // Calculate cost units using the same method as buildFlareTransaction - const feeSize = this.calculateImportCost(baseTx); - // Use integer division to ensure feeRate can be converted back to BigInt - const feeRate = Math.floor(Number(fee) / feeSize); - - this.transaction._fee = { - fee: fee.toString(), - feeRate: feeRate, - size: feeSize, - }; // Use credentials passed from TransactionBuilderFactory (properly extracted using codec) const credentials = parsedCredentials || []; @@ -88,6 +77,30 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { const inputThreshold = firstInput.sigIndicies().length || this.transaction._threshold; this.transaction._threshold = inputThreshold; + // Create a temporary UnsignedTx for accurate fee size calculation + // This includes the full structure (ImportTx, AddressMaps, Credentials) + const tempAddressMap = new FlareUtils.AddressMap(); + for (let i = 0; i < inputThreshold; i++) { + if (this.transaction._fromAddresses && this.transaction._fromAddresses[i]) { + tempAddressMap.set(new Address(this.transaction._fromAddresses[i]), i); + } + } + const tempAddressMaps = new FlareUtils.AddressMaps([tempAddressMap]); + const tempCredentials = + credentials.length > 0 ? credentials : [new Credential(Array(inputThreshold).fill(utils.createNewSig('')))]; + const tempUnsignedTx = new UnsignedTx(baseTx, [], tempAddressMaps, tempCredentials); + + // Calculate cost units using the full UnsignedTx structure + const feeSize = this.calculateImportCost(tempUnsignedTx); + // Use integer division to ensure feeRate can be converted back to BigInt + const feeRate = Math.floor(Number(fee) / feeSize); + + this.transaction._fee = { + fee: fee.toString(), + feeRate: feeRate, + size: feeSize, + }; + // Create AddressMaps based on signature slot order (matching credential order), not sorted addresses // This matches the approach used in credentials: addressesIndex determines signature order // AddressMaps should map addresses to signature slots in the same order as credentials @@ -166,7 +179,8 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { const { inputs, amount, credentials } = this.createInputs(); // Calculate import cost units (matching AVAXP's costImportTx approach) - // Create a temporary transaction with full amount to calculate fee size + // Create a temporary UnsignedTx with full amount to calculate fee size + // This includes the full structure (ImportTx, AddressMaps, Credentials) for accurate size calculation const tempOutput = new evmSerial.Output( new Address(this.transaction._to[0]), new BigIntPr(amount), @@ -180,8 +194,18 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { [tempOutput] ); - // Calculate feeSize once using full amount (matching AVAXP approach) - const feeSize = this.calculateImportCost(tempImportTx); + // Create AddressMaps for fee calculation (same as final transaction) + const firstUtxo = this.transaction._utxos[0]; + const tempAddressMap = firstUtxo + ? this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold) + : new FlareUtils.AddressMap(); + const tempAddressMaps = new FlareUtils.AddressMaps([tempAddressMap]); + + // Create temporary UnsignedTx with full structure for accurate fee calculation + const tempUnsignedTx = new UnsignedTx(tempImportTx, [], tempAddressMaps, credentials); + + // Calculate feeSize once using full UnsignedTx (matching AVAXP approach) + const feeSize = this.calculateImportCost(tempUnsignedTx); const feeRate = BigInt(this.transaction._fee.feeRate); const fee = feeRate * BigInt(feeSize); @@ -211,21 +235,11 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { [output] ); - // Create AddressMaps based on signature slot order (matching credential order), not sorted addresses - // This matches the approach used in credentials: addressesIndex determines signature order - // AddressMaps should map addresses to signature slots in the same order as credentials - // For C-chain imports, we typically have one input, so use the first UTXO - // Use centralized method for AddressMap creation - const firstUtxo = this.transaction._utxos[0]; - const addressMap = firstUtxo - ? this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold) - : new FlareUtils.AddressMap(); - const addressMaps = new FlareUtils.AddressMaps([addressMap]); - + // Reuse the AddressMaps already calculated for fee calculation const unsignedTx = new UnsignedTx( importTx, [], // Empty UTXOs array, will be filled during processing - addressMaps, + tempAddressMaps, credentials ); @@ -298,39 +312,21 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { } /** - * Calculate the import cost for C-chain import transactions - * Matches AVAXP's costImportTx formula: - * - Base byte cost: transactionSize * txBytesGas (1 gas per byte) - * - Per-input cost: numInputs * costPerSignature (1000 per signature) * threshold - * - Fixed fee: 10000 - * - * This returns cost "units" to be multiplied by feeRate, matching AVAXP's approach: - * AVAXP: fee = feeRate.muln(costImportTx(tx)) - * FLRP: fee = feeRate * calculateImportCost(tx) - * - * @param tx The ImportTx to calculate the cost for + * @param unsignedTx The UnsignedTx to calculate the cost for (includes ImportTx, AddressMaps, and Credentials) * @returns The total cost units */ - private calculateImportCost(tx: evmSerial.ImportTx): number { - const codec = avmSerial.getAVMManager().getDefaultCodec(); - const txBytes = tx.toBytes(codec); - - // Base byte cost: 1 gas per byte (matching AVAX txBytesGas) + private calculateImportCost(unsignedTx: UnsignedTx): number { + const signedTxBytes = unsignedTx.getSignedTx().toBytes(); const txBytesGas = 1; - let bytesCost = txBytes.length * txBytesGas; - - // Per-input cost: costPerSignature (1000) per signature + let bytesCost = signedTxBytes.length * txBytesGas; const costPerSignature = 1000; - const numInputs = tx.importedInputs.length; - const numSignatures = this.transaction._threshold; // Each input requires threshold signatures - const inputCost = numInputs * costPerSignature * numSignatures; - bytesCost += inputCost; - - // Fixed fee component + const importTx = unsignedTx.getTx() as evmSerial.ImportTx; + importTx.importedInputs.forEach((input: TransferableInput) => { + const inCost = costPerSignature * input.sigIndicies().length; + bytesCost += inCost; + }); const fixedFee = 10000; - const totalCost = bytesCost + fixedFee; - - return totalCost; + return bytesCost + fixedFee; } /** diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts b/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts index e743961af7..2b78e98501 100644 --- a/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts +++ b/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts @@ -1,13 +1,13 @@ export const IMPORT_IN_C = { - txhash: '2Q5RkxF2eRK3KCzDaijoScyunahbEvt6ai6YZipmShQTPryfky', + txhash: '5wgxtB8tSyS2MNroAQgs8fA9sDUWCwrGJG88b77xUm1po685Q', unsignedHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e15810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576b6e21', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e158100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ea9607a', halfSignedSignature: '0xd365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d00', halfSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000077fae449', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002e0c6d38e404ebeb08417a49c6c227e8c6ec5fd0c6a49d74eafa280cb8c6e19fe4d230834a0a3af5c3a49f3aefa9fc5f3d1be234aa8625f878634c9c079e58086010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000196ceb32', fullSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901d833ae918ca0bc59a4495e98837ffca0870666aaea0fbb8fd9b510e21e24f81071c2a622cd8979138e65ae413a0b1b573e2615dba04778a44f2b6c72566dd13401d6bc2f0a', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002e0c6d38e404ebeb08417a49c6c227e8c6ec5fd0c6a49d74eafa280cb8c6e19fe4d230834a0a3af5c3a49f3aefa9fc5f3d1be234aa8625f878634c9c079e5808601ecb60f57234c5e852add6fbbf90f2c8613b4b1a520aefe6f7e78b5e21155d1f131107cf9177764c8dda5936e1dd8d5846ea9b7c016e3811565ab8b48776a15a100bb68cc63', fullSignedSignature: '0x70d2ca9711622142610ddd347e482cbe5dc45aeafe66876bb82bfd57581300045b8457d804cc1b8f2efc10401367e5919b1912ee26d2d48c06cf82dc3f146acd00', @@ -39,7 +39,7 @@ export const IMPORT_IN_C = { to: '0x17Dbd11B9dD1c9bE337353db7C14f9fb3662E5B5', sourceChainId: 'vE8M98mEQH6wk56sStD1ML8HApTgSqfJZLk9gQ3Fsd4i6m3Bi', threshold: 2, - fee: '409', // feeRate multiplier: 5,000,000 (desired fee) ÷ ~12,228 (cost units) ≈ 409 + fee: '550', locktime: 0, INVALID_CHAIN_ID: 'wrong chain id', VALID_C_CHAIN_ID: 'yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp', diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts index b998f4b5be..78714ce6c6 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts @@ -52,84 +52,105 @@ describe('Flrp Import In C Tx Builder', () => { txHash: testData.txhash, }); - describe('dynamic fee calculation', () => { - it('should calculate proper fee using feeRate multiplier (AVAXP approach) to avoid "insufficient unlocked funds" error', async () => { - const amount = '100000000'; // 100M nanoFLRP (0.1 FLR) - const feeRate = '1'; // 1 nanoFLRP per cost unit (matching AVAXP's feeRate usage) + describe('fee calculation - insufficient unlocked funds fix', () => { + /** + * This test verifies the fix for the "insufficient unlocked funds" error that occurred + * during P-to-C chain transactions. + * + * Real-world transaction data: + * - Input: 100,000,000 nanoFLRP (from P-chain export) + * - Original feeRate: 500, which caused "needs 280000 more" error + * - Old (buggy) calculation: + * - Size: 12,234 (only unsignedTx.toBytes()) + * - Fee: 500 × 12,234 = 6,117,000 + * - Error: "insufficient unlocked funds: needs 280000 more" + * - Required fee: 6,117,000 + 280,000 = 6,397,000 + * + * The fix has two parts: + * 1. Use getSignedTx().toBytes() to include credentials in size calculation (~140+ bytes) + * 2. Increase feeRate from 500 to 550 to provide additional buffer + * + * With fix: size ~12,376 × feeRate 550 = 6,806,800 > 6,397,000 ✓ + */ + it('should calculate sufficient fee to avoid "insufficient unlocked funds" error', async () => { + const inputAmount = '100000000'; + const feeRate = 550; + const threshold = 2; + + const pAddresses = [ + 'P-costwo1060n6skw5lsz7ch8z4vnv2s24vetjv5w73g4k2', + 'P-costwo1kt5hrl4kr5dt92ayxjash6uujkf4nh5ex0y9rj', + 'P-costwo1eys86hynecjn8400j30e7y706aecv8wz0l875x', + ]; + + const cChainDestination = '0x7e9f3d42cea7e02f62e71559362a0aab32b9328e'; const utxo: DecodedUtxoObj = { - outputID: 0, - amount: amount, - txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP', + outputID: 7, + amount: inputAmount, + txid: '2b2A4CyaRawiVAycUhpfvaxizymUT3TwRUbrzwiy3qp7DnKznj', outputidx: '0', addresses: [ - '0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581', - '0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001', - '0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91', + '0x7e9f3d42cea7e02f62e71559362a0aab32b9328e', + 'C9207d5c93ce2533d5ef945f9f13cfd773861dc2', + 'b2e971feb61d1ab2aba434bb0beb9c959359de99', ], - threshold: 2, + threshold: threshold, }; const txBuilder = factory .getImportInCBuilder() - .threshold(2) - .fromPubKey(testData.pAddresses) + .threshold(threshold) + .fromPubKey(pAddresses) .utxos([utxo]) - .to(testData.to) - .feeRate(feeRate) as any; + .to(cChainDestination) + .feeRate(feeRate.toString()); const tx = await txBuilder.build(); + const feeInfo = (tx as any).fee; + const calculatedFee = BigInt(feeInfo.fee); + const calculatedSize = feeInfo.size; - const calculatedFee = BigInt((tx as any).fee.fee); - const feeRateBigInt = BigInt(feeRate); - - // The fee should be approximately: feeRate × (txSize + inputCost + fixedFee) - // For 1 input, threshold=2, ~228 bytes: 1 × (228 + 2000 + 10000) = 12,228 - const expectedMinCost = 12000; // Minimum cost units (conservative estimate) - const expectedMaxCost = 13000; // Maximum cost units (with some buffer) - - const expectedMinFee = feeRateBigInt * BigInt(expectedMinCost); - const expectedMaxFee = feeRateBigInt * BigInt(expectedMaxCost); + const oldBuggyFeeAt500 = BigInt(12234) * BigInt(500); + const shortfall = BigInt(280000); + const requiredFee = oldBuggyFeeAt500 + shortfall; - // Verify fee is in the expected range assert( - calculatedFee >= expectedMinFee, - `Fee ${calculatedFee} should be at least ${expectedMinFee} (feeRate × minCost)` + calculatedFee >= requiredFee, + `Fee ${calculatedFee} should be at least ${requiredFee} (old fee ${oldBuggyFeeAt500} + shortfall ${shortfall})` ); + + const oldBuggySize = 12234; assert( - calculatedFee <= expectedMaxFee, - `Fee ${calculatedFee} should not exceed ${expectedMaxFee} (feeRate × maxCost)` + calculatedSize > oldBuggySize, + `Size ${calculatedSize} should be greater than old buggy size ${oldBuggySize}` ); - // Verify the output amount is positive (no "insufficient funds" error) - const outputs = tx.outputs; - outputs.length.should.equal(1); - const outputAmount = BigInt(outputs[0].value); + const outputAmount = BigInt(tx.outputs[0].value); + assert(outputAmount > BigInt(0), 'Output amount should be positive'); + + const inputBigInt = BigInt(inputAmount); + const expectedOutput = inputBigInt - calculatedFee; assert( - outputAmount > BigInt(0), - 'Output amount should be positive - transaction should not fail with insufficient funds' + outputAmount === expectedOutput, + `Output ${outputAmount} should equal input ${inputBigInt} minus fee ${calculatedFee}` ); - - // Verify the math: input - output = fee - const inputAmount = BigInt(amount); - const calculatedOutput = inputAmount - calculatedFee; - assert(outputAmount === calculatedOutput, 'Output should equal input minus total fee'); }); - it('should use consistent fee calculation in initBuilder and buildFlareTransaction', async () => { - const inputAmount = '100000000'; // 100M nanoFLRP (matches real-world transaction) - const expectedFeeRate = 500; // Real feeRate from working transaction + it('should match AVAXP costImportTx formula: bytesCost + inputCosts + fixedFee', async () => { + const inputAmount = '100000000'; + const feeRate = 500; const threshold = 2; const utxo: DecodedUtxoObj = { - outputID: 0, + outputID: 7, amount: inputAmount, - txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP', + txid: '2b2A4CyaRawiVAycUhpfvaxizymUT3TwRUbrzwiy3qp7DnKznj', outputidx: '0', addresses: [ - '0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581', - '0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001', - '0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91', + '0x7e9f3d42cea7e02f62e71559362a0aab32b9328e', + 'C9207d5c93ce2533d5ef945f9f13cfd773861dc2', + 'b2e971feb61d1ab2aba434bb0beb9c959359de99', ], threshold: threshold, }; @@ -140,71 +161,48 @@ describe('Flrp Import In C Tx Builder', () => { .fromPubKey(testData.pAddresses) .utxos([utxo]) .to(testData.to) - .feeRate(expectedFeeRate.toString()); + .feeRate(feeRate.toString()); const tx = await txBuilder.build(); - const calculatedFee = BigInt((tx as any).fee.fee); const feeInfo = (tx as any).fee; + const calculatedSize = feeInfo.size; - const maxReasonableFee = BigInt(inputAmount) / BigInt(10); // Max 10% of input - assert( - calculatedFee < maxReasonableFee, - `Fee ${calculatedFee} should be less than 10% of input (${maxReasonableFee})` - ); + const expectedInputCost = 1000 * threshold; + const fixedFee = 10000; + const expectedMinBytesCost = 200; - const expectedMinFee = BigInt(expectedFeeRate) * BigInt(12000); - const expectedMaxFee = BigInt(expectedFeeRate) * BigInt(13000); - - assert(calculatedFee >= expectedMinFee, `Fee ${calculatedFee} should be at least ${expectedMinFee}`); - assert(calculatedFee <= expectedMaxFee, `Fee ${calculatedFee} should not exceed ${expectedMaxFee}`); - - const outputAmount = BigInt(tx.outputs[0].value); - assert(outputAmount > BigInt(0), 'Output should be positive'); + const impliedBytesCost = calculatedSize - expectedInputCost - fixedFee; - const expectedOutput = BigInt(inputAmount) - calculatedFee; assert( - outputAmount === expectedOutput, - `Output ${outputAmount} should equal input ${inputAmount} minus fee ${calculatedFee}` + impliedBytesCost >= expectedMinBytesCost, + `Implied bytes cost ${impliedBytesCost} should be at least ${expectedMinBytesCost}` ); - const txHex = tx.toBroadcastFormat(); - const parsedBuilder = factory.from(txHex); - const parsedTx = await parsedBuilder.build(); - const parsedFeeRate = (parsedTx as any).fee.feeRate; - - assert(parsedFeeRate !== undefined && parsedFeeRate > 0, 'Parsed feeRate should be defined and positive'); - - const feeRateDiff = Math.abs(parsedFeeRate! - expectedFeeRate); - const maxAllowedDiff = 10; + const expectedMinTotalSize = expectedMinBytesCost + expectedInputCost + fixedFee; assert( - feeRateDiff <= maxAllowedDiff, - `Parsed feeRate ${parsedFeeRate} should be close to original ${expectedFeeRate} (diff: ${feeRateDiff})` + calculatedSize >= expectedMinTotalSize, + `Total size ${calculatedSize} should be at least ${expectedMinTotalSize} (bytes + inputCost + fixedFee)` ); - - const feeSize = feeInfo.size!; - assert(feeSize > 10000, `Fee size ${feeSize} should include fixed cost (10000) + input costs`); - assert(feeSize < 20000, `Fee size ${feeSize} should be reasonable (< 20000)`); }); - it('should prevent artificially inflated feeRate from using wrong calculation', async () => { - const inputAmount = '100000000'; // 100M nanoFLRP + it('should produce consistent fees between build and parse (initBuilder vs buildFlareTransaction)', async () => { + const inputAmount = '100000000'; + const feeRate = 500; const threshold = 2; const utxo: DecodedUtxoObj = { - outputID: 0, + outputID: 7, amount: inputAmount, - txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP', + txid: '2b2A4CyaRawiVAycUhpfvaxizymUT3TwRUbrzwiy3qp7DnKznj', outputidx: '0', addresses: [ - '0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581', - '0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001', - '0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91', + '0x7e9f3d42cea7e02f62e71559362a0aab32b9328e', + 'C9207d5c93ce2533d5ef945f9f13cfd773861dc2', + 'b2e971feb61d1ab2aba434bb0beb9c959359de99', ], threshold: threshold, }; - const feeRate = 500; - const txBuilder = factory .getImportInCBuilder() .threshold(threshold) @@ -213,30 +211,29 @@ describe('Flrp Import In C Tx Builder', () => { .to(testData.to) .feeRate(feeRate.toString()); - let tx; - try { - tx = await txBuilder.build(); - } catch (error: any) { - throw new Error( - `Transaction build failed (this was the OLD bug behavior): ${error.message}. ` + - `The fix ensures calculateImportCost() is used consistently.` - ); - } - - const calculatedFee = BigInt((tx as any).fee.fee); + const originalTx = await txBuilder.build(); + const originalFeeInfo = (originalTx as any).fee; + const originalSize = originalFeeInfo.size; - const oldBugFee = BigInt(328000000); - const reasonableFee = BigInt(10000000); + const txHex = originalTx.toBroadcastFormat(); + const parsedBuilder = factory.from(txHex); + const parsedTx = await parsedBuilder.build(); + const parsedFeeInfo = (parsedTx as any).fee; + const parsedFeeRate = parsedFeeInfo.feeRate; + const parsedSize = parsedFeeInfo.size; + const feeRateDiff = Math.abs(parsedFeeRate - feeRate); + const maxAllowedDiff = 50; assert( - calculatedFee < reasonableFee, - `Fee ${calculatedFee} should be reasonable (< ${reasonableFee}), not inflated like OLD bug (~${oldBugFee})` + feeRateDiff <= maxAllowedDiff, + `Parsed feeRate ${parsedFeeRate} should be close to original ${feeRate} (diff: ${feeRateDiff})` ); - const outputAmount = BigInt(tx.outputs[0].value); + const sizeDiff = Math.abs(parsedSize - originalSize); + const maxSizeDiff = 100; assert( - outputAmount > BigInt(0), - `Output ${outputAmount} should be positive. OLD bug would make output negative due to excessive fee.` + sizeDiff <= maxSizeDiff, + `Parsed size ${parsedSize} should be close to original ${originalSize} (diff: ${sizeDiff})` ); }); });