Skip to content

Commit f0c94d5

Browse files
authored
Merge pull request #7823 from BitGo/WIN-8488
refactor(sdk-coin-flrp): update import transaction fee calculation method
2 parents 4e272e1 + df211a3 commit f0c94d5

File tree

2 files changed

+135
-20
lines changed

2 files changed

+135
-20
lines changed

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

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
6363

6464
// Calculate fee based on input/output difference
6565
const fee = totalInputAmount - totalOutputAmount;
66-
const feeSize = this.calculateFeeSize(baseTx);
66+
// Calculate cost units using the same method as buildFlareTransaction
67+
const feeSize = this.calculateImportCost(baseTx);
6768
// Use integer division to ensure feeRate can be converted back to BigInt
6869
const feeRate = Math.floor(Number(fee) / feeSize);
6970

@@ -165,7 +166,7 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
165166
const { inputs, amount, credentials } = this.createInputs();
166167

167168
// Calculate import cost units (matching AVAXP's costImportTx approach)
168-
// Create a temporary transaction to calculate the actual cost units
169+
// Create a temporary transaction with full amount to calculate fee size
169170
const tempOutput = new evmSerial.Output(
170171
new Address(this.transaction._to[0]),
171172
new BigIntPr(amount),
@@ -179,13 +180,18 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
179180
[tempOutput]
180181
);
181182

182-
// Calculate the import cost units (matches AVAXP's feeSize from costImportTx)
183+
// Calculate feeSize once using full amount (matching AVAXP approach)
183184
const feeSize = this.calculateImportCost(tempImportTx);
184-
185-
// Multiply feeRate by cost units (matching AVAXP: fee = feeRate.muln(feeSize))
186185
const feeRate = BigInt(this.transaction._fee.feeRate);
187186
const fee = feeRate * BigInt(feeSize);
188187

188+
// Validate that we have enough funds to cover the fee
189+
if (amount <= fee) {
190+
throw new BuildTransactionError(
191+
`Insufficient funds: have ${amount.toString()}, need more than ${fee.toString()} for fee`
192+
);
193+
}
194+
189195
this.transaction._fee.fee = fee.toString();
190196
this.transaction._fee.size = feeSize;
191197

@@ -327,21 +333,6 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
327333
return totalCost;
328334
}
329335

330-
/**
331-
* Calculate the fee size for the transaction (for backwards compatibility)
332-
* For C-chain imports, the feeRate is treated as an absolute fee value
333-
*/
334-
private calculateFeeSize(tx?: evmSerial.ImportTx): number {
335-
// If tx is provided, calculate based on actual transaction size
336-
if (tx) {
337-
const codec = avmSerial.getAVMManager().getDefaultCodec();
338-
return tx.toBytes(codec).length;
339-
}
340-
341-
// For C-chain imports, treat feeRate as the absolute fee (multiplier of 1)
342-
return 1;
343-
}
344-
345336
/**
346337
* Recover UTXOs from imported inputs
347338
* @param importedInputs Array of transferable inputs

modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,130 @@ describe('Flrp Import In C Tx Builder', () => {
115115
const calculatedOutput = inputAmount - calculatedFee;
116116
assert(outputAmount === calculatedOutput, 'Output should equal input minus total fee');
117117
});
118+
119+
it('should use consistent fee calculation in initBuilder and buildFlareTransaction', async () => {
120+
const inputAmount = '100000000'; // 100M nanoFLRP (matches real-world transaction)
121+
const expectedFeeRate = 500; // Real feeRate from working transaction
122+
const threshold = 2;
123+
124+
const utxo: DecodedUtxoObj = {
125+
outputID: 0,
126+
amount: inputAmount,
127+
txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP',
128+
outputidx: '0',
129+
addresses: [
130+
'0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581',
131+
'0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001',
132+
'0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91',
133+
],
134+
threshold: threshold,
135+
};
136+
137+
const txBuilder = factory
138+
.getImportInCBuilder()
139+
.threshold(threshold)
140+
.fromPubKey(testData.pAddresses)
141+
.utxos([utxo])
142+
.to(testData.to)
143+
.feeRate(expectedFeeRate.toString());
144+
145+
const tx = await txBuilder.build();
146+
const calculatedFee = BigInt((tx as any).fee.fee);
147+
const feeInfo = (tx as any).fee;
148+
149+
const maxReasonableFee = BigInt(inputAmount) / BigInt(10); // Max 10% of input
150+
assert(
151+
calculatedFee < maxReasonableFee,
152+
`Fee ${calculatedFee} should be less than 10% of input (${maxReasonableFee})`
153+
);
154+
155+
const expectedMinFee = BigInt(expectedFeeRate) * BigInt(12000);
156+
const expectedMaxFee = BigInt(expectedFeeRate) * BigInt(13000);
157+
158+
assert(calculatedFee >= expectedMinFee, `Fee ${calculatedFee} should be at least ${expectedMinFee}`);
159+
assert(calculatedFee <= expectedMaxFee, `Fee ${calculatedFee} should not exceed ${expectedMaxFee}`);
160+
161+
const outputAmount = BigInt(tx.outputs[0].value);
162+
assert(outputAmount > BigInt(0), 'Output should be positive');
163+
164+
const expectedOutput = BigInt(inputAmount) - calculatedFee;
165+
assert(
166+
outputAmount === expectedOutput,
167+
`Output ${outputAmount} should equal input ${inputAmount} minus fee ${calculatedFee}`
168+
);
169+
170+
const txHex = tx.toBroadcastFormat();
171+
const parsedBuilder = factory.from(txHex);
172+
const parsedTx = await parsedBuilder.build();
173+
const parsedFeeRate = (parsedTx as any).fee.feeRate;
174+
175+
assert(parsedFeeRate !== undefined && parsedFeeRate > 0, 'Parsed feeRate should be defined and positive');
176+
177+
const feeRateDiff = Math.abs(parsedFeeRate! - expectedFeeRate);
178+
const maxAllowedDiff = 10;
179+
assert(
180+
feeRateDiff <= maxAllowedDiff,
181+
`Parsed feeRate ${parsedFeeRate} should be close to original ${expectedFeeRate} (diff: ${feeRateDiff})`
182+
);
183+
184+
const feeSize = feeInfo.size!;
185+
assert(feeSize > 10000, `Fee size ${feeSize} should include fixed cost (10000) + input costs`);
186+
assert(feeSize < 20000, `Fee size ${feeSize} should be reasonable (< 20000)`);
187+
});
188+
189+
it('should prevent artificially inflated feeRate from using wrong calculation', async () => {
190+
const inputAmount = '100000000'; // 100M nanoFLRP
191+
const threshold = 2;
192+
193+
const utxo: DecodedUtxoObj = {
194+
outputID: 0,
195+
amount: inputAmount,
196+
txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP',
197+
outputidx: '0',
198+
addresses: [
199+
'0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581',
200+
'0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001',
201+
'0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91',
202+
],
203+
threshold: threshold,
204+
};
205+
206+
const feeRate = 500;
207+
208+
const txBuilder = factory
209+
.getImportInCBuilder()
210+
.threshold(threshold)
211+
.fromPubKey(testData.pAddresses)
212+
.utxos([utxo])
213+
.to(testData.to)
214+
.feeRate(feeRate.toString());
215+
216+
let tx;
217+
try {
218+
tx = await txBuilder.build();
219+
} catch (error: any) {
220+
throw new Error(
221+
`Transaction build failed (this was the OLD bug behavior): ${error.message}. ` +
222+
`The fix ensures calculateImportCost() is used consistently.`
223+
);
224+
}
225+
226+
const calculatedFee = BigInt((tx as any).fee.fee);
227+
228+
const oldBugFee = BigInt(328000000);
229+
const reasonableFee = BigInt(10000000);
230+
231+
assert(
232+
calculatedFee < reasonableFee,
233+
`Fee ${calculatedFee} should be reasonable (< ${reasonableFee}), not inflated like OLD bug (~${oldBugFee})`
234+
);
235+
236+
const outputAmount = BigInt(tx.outputs[0].value);
237+
assert(
238+
outputAmount > BigInt(0),
239+
`Output ${outputAmount} should be positive. OLD bug would make output negative due to excessive fee.`
240+
);
241+
});
118242
});
119243

120244
describe('on-chain verified transactions', () => {

0 commit comments

Comments
 (0)