From 689738d8d21e3c7e41aed02bf75f77075418db3c Mon Sep 17 00:00:00 2001 From: Kashif Jamil Date: Mon, 29 Dec 2025 10:38:04 +0530 Subject: [PATCH] fix(sdk-coin-flrp): implement dynamic fee calculation for C-chain import transactions Ticket: WIN-8461 --- .../src/lib/ImportInCTxBuilder.ts | 62 ++++++++++++++++-- .../resources/transactionData/importInC.ts | 10 +-- .../test/unit/lib/importInCTxBuilder.ts | 65 +++++++++++++++++++ 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts index 42b4baf260..a51d41a806 100644 --- a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts @@ -164,14 +164,32 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { const { inputs, amount, credentials } = this.createInputs(); - // Calculate fee + // Calculate import cost units (matching AVAXP's costImportTx approach) + // Create a temporary transaction to calculate the actual cost units + const tempOutput = new evmSerial.Output( + new Address(this.transaction._to[0]), + new BigIntPr(amount), + new Id(new Uint8Array(Buffer.from(this.transaction._assetId, 'hex'))) + ); + const tempImportTx = new evmSerial.ImportTx( + new Int(this.transaction._networkID), + new Id(new Uint8Array(Buffer.from(this.transaction._blockchainID, 'hex'))), + new Id(new Uint8Array(this._externalChainId)), + inputs, + [tempOutput] + ); + + // Calculate the import cost units (matches AVAXP's feeSize from costImportTx) + const feeSize = this.calculateImportCost(tempImportTx); + + // Multiply feeRate by cost units (matching AVAXP: fee = feeRate.muln(feeSize)) const feeRate = BigInt(this.transaction._fee.feeRate); - const feeSize = this.calculateFeeSize(); const fee = feeRate * BigInt(feeSize); + this.transaction._fee.fee = fee.toString(); this.transaction._fee.size = feeSize; - // Create EVM output using proper FlareJS class + // Create EVM output using proper FlareJS class with amount minus fee const output = new evmSerial.Output( new Address(this.transaction._to[0]), new BigIntPr(amount - fee), @@ -274,7 +292,43 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { } /** - * Calculate the fee size for the transaction + * 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 + * @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) + const txBytesGas = 1; + let bytesCost = txBytes.length * txBytesGas; + + // Per-input cost: costPerSignature (1000) per signature + 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 fixedFee = 10000; + const totalCost = bytesCost + fixedFee; + + return totalCost; + } + + /** + * Calculate the fee size for the transaction (for backwards compatibility) * For C-chain imports, the feeRate is treated as an absolute fee value */ private calculateFeeSize(tx?: evmSerial.ImportTx): number { diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts b/modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts index 60bd5e4038..e743961af7 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: '2we2yuz575k7BnVgX4AdiheL9xTDpVSi8f8tRpD4C7SwPxR7YB', + txhash: '2Q5RkxF2eRK3KCzDaijoScyunahbEvt6ai6YZipmShQTPryfky', unsignedHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e158100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003452faa4', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e15810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576b6e21', halfSignedSignature: '0xd365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d00', halfSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002d365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a711bae5', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000077fae449', fullSigntxHex: - '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002d365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d0070d2ca9711622142610ddd347e482cbe5dc45aeafe66876bb82bfd57581300045b8457d804cc1b8f2efc10401367e5919b1912ee26d2d48c06cf82dc3f146acd002e7749e9', + '0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901d833ae918ca0bc59a4495e98837ffca0870666aaea0fbb8fd9b510e21e24f81071c2a622cd8979138e65ae413a0b1b573e2615dba04778a44f2b6c72566dd13401d6bc2f0a', fullSignedSignature: '0x70d2ca9711622142610ddd347e482cbe5dc45aeafe66876bb82bfd57581300045b8457d804cc1b8f2efc10401367e5919b1912ee26d2d48c06cf82dc3f146acd00', @@ -39,7 +39,7 @@ export const IMPORT_IN_C = { to: '0x17Dbd11B9dD1c9bE337353db7C14f9fb3662E5B5', sourceChainId: 'vE8M98mEQH6wk56sStD1ML8HApTgSqfJZLk9gQ3Fsd4i6m3Bi', threshold: 2, - fee: '5000000', + fee: '409', // feeRate multiplier: 5,000,000 (desired fee) ÷ ~12,228 (cost units) ≈ 409 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 6af56a1a2b..922421cca0 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts @@ -52,6 +52,71 @@ 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) + + const utxo: DecodedUtxoObj = { + outputID: 0, + amount: amount, + txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP', + outputidx: '0', + addresses: [ + '0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581', + '0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001', + '0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91', + ], + threshold: 2, + }; + + const txBuilder = factory + .getImportInCBuilder() + .threshold(2) + .fromPubKey(testData.pAddresses) + .utxos([utxo]) + .to(testData.to) + .feeRate(feeRate) as any; + + const tx = await txBuilder.build(); + + 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); + + // Verify fee is in the expected range + assert( + calculatedFee >= expectedMinFee, + `Fee ${calculatedFee} should be at least ${expectedMinFee} (feeRate × minCost)` + ); + assert( + calculatedFee <= expectedMaxFee, + `Fee ${calculatedFee} should not exceed ${expectedMaxFee} (feeRate × maxCost)` + ); + + // 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); + assert( + outputAmount > BigInt(0), + 'Output amount should be positive - transaction should not fail with insufficient funds' + ); + + // Verify the math: input - output = fee + const inputAmount = BigInt(amount); + const calculatedOutput = inputAmount - calculatedFee; + assert(outputAmount === calculatedOutput, 'Output should equal input minus total fee'); + }); + }); + describe('on-chain verified transactions', () => { it('should verify on-chain tx id for signed C-chain import', async () => { const signedImportHex =