From d9434aa02b1f302da0c7a67b0b349d79eb8c9721 Mon Sep 17 00:00:00 2001 From: "arunchockalingam504@bitgo.com" Date: Thu, 25 Jun 2026 15:15:10 +0000 Subject: [PATCH] fix(sdk-coin-flrp): subtract minImportToPFee from ExportInC outputAmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For C→P imports, the wallet platform shows the ExportInC transaction as the pending import amount on the FLRP (P-chain) wallet before the ImportInP is confirmed. The ExportInC exportedOutputs hold the gross P-chain UTXO amount (which includes the import fee premium), causing the pending display to show a higher amount than the actual P-chain credit delivered by the ImportInP. Fix: in `Flrp.explainTransaction`, when the transaction is a C-chain export (ExportInC), subtract the network's `minImportToPFee` from the `outputAmount` and each output's amount. This yields the minimum expected net P-chain receipt, aligning the pending display with the confirmed P-chain balance and resolving the amount discrepancy reported in CECHO-1450. The `verifyTransaction` path continues to use the gross UTXO amount for validation (via `tx.explainTransaction()` directly), so existing export validation logic is unaffected. Ticket: CECHO-1450 Session-Id: 53bbdb3d-3f67-4bc4-b5cd-a8be1b2d8857 Task-Id: 99e7fafb-287d-487c-b5fb-9c7f4c2f28f7 --- modules/sdk-coin-flrp/src/flrp.ts | 26 +++++++++++++++++++++++-- modules/sdk-coin-flrp/test/unit/flrp.ts | 18 +++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/modules/sdk-coin-flrp/src/flrp.ts b/modules/sdk-coin-flrp/src/flrp.ts index 951f4a03f3..2848b31390 100644 --- a/modules/sdk-coin-flrp/src/flrp.ts +++ b/modules/sdk-coin-flrp/src/flrp.ts @@ -428,8 +428,30 @@ export class Flrp extends BaseCoin { } try { const txBuilder = this.getBuilder().from(txHex); - const tx = await txBuilder.build(); - return tx.explainTransaction(); + const tx = (await txBuilder.build()) as FlrpLib.Transaction; + const explanation = tx.explainTransaction(); + + // For a C→P export (ExportInC viewed from the FLRP P-chain perspective), the + // exportedOutputs contain the P-chain UTXO amount, which is the gross amount + // BEFORE the import fee is deducted. The platform uses this explanation to show + // the "pending import amount" before the ImportInP is confirmed. To avoid + // displaying a higher-than-actual amount, subtract the minimum import-to-P fee + // so the displayed amount reflects the expected net P-chain receipt. + if (tx.isTransactionForCChain && explanation.type === TransactionType.Export) { + const minImportToPFee = BigInt((this._staticsCoin.network as FlareNetwork).minImportToPFee); + const adjustedOutputs = explanation.outputs.map((o) => ({ + ...o, + amount: (BigInt(o.amount) - minImportToPFee).toString(), + })); + const adjustedOutputAmount = (BigInt(explanation.outputAmount) - minImportToPFee).toString(); + return { + ...explanation, + outputs: adjustedOutputs, + outputAmount: adjustedOutputAmount, + }; + } + + return explanation; } catch (e) { throw new Error(`Invalid transaction: ${e.message}`); } diff --git a/modules/sdk-coin-flrp/test/unit/flrp.ts b/modules/sdk-coin-flrp/test/unit/flrp.ts index 74cb887464..f44c526720 100644 --- a/modules/sdk-coin-flrp/test/unit/flrp.ts +++ b/modules/sdk-coin-flrp/test/unit/flrp.ts @@ -295,6 +295,24 @@ describe('Flrp test cases', function () { txExplain.changeOutputs.should.be.empty(); }); + it('should subtract minImportToPFee from ExportInC outputAmount to show expected net P-chain receipt', async () => { + // The ExportInC UTXO amount is the gross amount going to P-chain. The actual P-chain + // credit will be UTXO - importFee. We subtract minImportToPFee to show the minimum + // net amount the user will receive on P-chain, preventing the pending display from + // showing an inflated amount compared to the confirmed P-chain balance. + const minImportToPFee = BigInt('1261000'); // from FlarePTestnet.minImportToPFee + const expectedAdjustedAmount = (BigInt(EXPORT_IN_C.amount) - minImportToPFee).toString(); + + // Should work for both unsigned (pending) and signed ExportInC hex + const unsignedExplain = await basecoin.explainTransaction({ txHex: EXPORT_IN_C.unsignedHex }); + unsignedExplain.outputAmount.should.equal(expectedAdjustedAmount); + unsignedExplain.outputs[0].amount.should.equal(expectedAdjustedAmount); + + const signedExplain = await basecoin.explainTransaction({ txHex: EXPORT_IN_C.signedHex }); + signedExplain.outputAmount.should.equal(expectedAdjustedAmount); + signedExplain.outputs[0].amount.should.equal(expectedAdjustedAmount); + }); + it('should fail when transaction hex is not provided', async () => { await basecoin.explainTransaction({}).should.be.rejectedWith('missing transaction hex'); });