Skip to content

Commit bbf141f

Browse files
authored
Merge pull request #7812 from BitGo/WIN-8461
fix(sdk-coin-flrp): implement dynamic fee calculation for C-chain import transactions
2 parents 3faaabf + 689738d commit bbf141f

File tree

3 files changed

+128
-9
lines changed

3 files changed

+128
-9
lines changed

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,32 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
164164

165165
const { inputs, amount, credentials } = this.createInputs();
166166

167-
// Calculate fee
167+
// Calculate import cost units (matching AVAXP's costImportTx approach)
168+
// Create a temporary transaction to calculate the actual cost units
169+
const tempOutput = new evmSerial.Output(
170+
new Address(this.transaction._to[0]),
171+
new BigIntPr(amount),
172+
new Id(new Uint8Array(Buffer.from(this.transaction._assetId, 'hex')))
173+
);
174+
const tempImportTx = new evmSerial.ImportTx(
175+
new Int(this.transaction._networkID),
176+
new Id(new Uint8Array(Buffer.from(this.transaction._blockchainID, 'hex'))),
177+
new Id(new Uint8Array(this._externalChainId)),
178+
inputs,
179+
[tempOutput]
180+
);
181+
182+
// Calculate the import cost units (matches AVAXP's feeSize from costImportTx)
183+
const feeSize = this.calculateImportCost(tempImportTx);
184+
185+
// Multiply feeRate by cost units (matching AVAXP: fee = feeRate.muln(feeSize))
168186
const feeRate = BigInt(this.transaction._fee.feeRate);
169-
const feeSize = this.calculateFeeSize();
170187
const fee = feeRate * BigInt(feeSize);
188+
171189
this.transaction._fee.fee = fee.toString();
172190
this.transaction._fee.size = feeSize;
173191

174-
// Create EVM output using proper FlareJS class
192+
// Create EVM output using proper FlareJS class with amount minus fee
175193
const output = new evmSerial.Output(
176194
new Address(this.transaction._to[0]),
177195
new BigIntPr(amount - fee),
@@ -274,7 +292,43 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
274292
}
275293

276294
/**
277-
* Calculate the fee size for the transaction
295+
* Calculate the import cost for C-chain import transactions
296+
* Matches AVAXP's costImportTx formula:
297+
* - Base byte cost: transactionSize * txBytesGas (1 gas per byte)
298+
* - Per-input cost: numInputs * costPerSignature (1000 per signature) * threshold
299+
* - Fixed fee: 10000
300+
*
301+
* This returns cost "units" to be multiplied by feeRate, matching AVAXP's approach:
302+
* AVAXP: fee = feeRate.muln(costImportTx(tx))
303+
* FLRP: fee = feeRate * calculateImportCost(tx)
304+
*
305+
* @param tx The ImportTx to calculate the cost for
306+
* @returns The total cost units
307+
*/
308+
private calculateImportCost(tx: evmSerial.ImportTx): number {
309+
const codec = avmSerial.getAVMManager().getDefaultCodec();
310+
const txBytes = tx.toBytes(codec);
311+
312+
// Base byte cost: 1 gas per byte (matching AVAX txBytesGas)
313+
const txBytesGas = 1;
314+
let bytesCost = txBytes.length * txBytesGas;
315+
316+
// Per-input cost: costPerSignature (1000) per signature
317+
const costPerSignature = 1000;
318+
const numInputs = tx.importedInputs.length;
319+
const numSignatures = this.transaction._threshold; // Each input requires threshold signatures
320+
const inputCost = numInputs * costPerSignature * numSignatures;
321+
bytesCost += inputCost;
322+
323+
// Fixed fee component
324+
const fixedFee = 10000;
325+
const totalCost = bytesCost + fixedFee;
326+
327+
return totalCost;
328+
}
329+
330+
/**
331+
* Calculate the fee size for the transaction (for backwards compatibility)
278332
* For C-chain imports, the feeRate is treated as an absolute fee value
279333
*/
280334
private calculateFeeSize(tx?: evmSerial.ImportTx): number {

modules/sdk-coin-flrp/test/resources/transactionData/importInC.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export const IMPORT_IN_C = {
2-
txhash: '2we2yuz575k7BnVgX4AdiheL9xTDpVSi8f8tRpD4C7SwPxR7YB',
2+
txhash: '2Q5RkxF2eRK3KCzDaijoScyunahbEvt6ai6YZipmShQTPryfky',
33
unsignedHex:
4-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e158100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003452faa4',
4+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e15810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576b6e21',
55
halfSignedSignature:
66
'0xd365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d00',
77
halfSigntxHex:
8-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002d365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a711bae5',
8+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000077fae449',
99
fullSigntxHex:
10-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8119c058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002d365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d0070d2ca9711622142610ddd347e482cbe5dc45aeafe66876bb82bfd57581300045b8457d804cc1b8f2efc10401367e5919b1912ee26d2d48c06cf82dc3f146acd002e7749e9',
10+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901d833ae918ca0bc59a4495e98837ffca0870666aaea0fbb8fd9b510e21e24f81071c2a622cd8979138e65ae413a0b1b573e2615dba04778a44f2b6c72566dd13401d6bc2f0a',
1111
fullSignedSignature:
1212
'0x70d2ca9711622142610ddd347e482cbe5dc45aeafe66876bb82bfd57581300045b8457d804cc1b8f2efc10401367e5919b1912ee26d2d48c06cf82dc3f146acd00',
1313

@@ -39,7 +39,7 @@ export const IMPORT_IN_C = {
3939
to: '0x17Dbd11B9dD1c9bE337353db7C14f9fb3662E5B5',
4040
sourceChainId: 'vE8M98mEQH6wk56sStD1ML8HApTgSqfJZLk9gQ3Fsd4i6m3Bi',
4141
threshold: 2,
42-
fee: '5000000',
42+
fee: '409', // feeRate multiplier: 5,000,000 (desired fee) ÷ ~12,228 (cost units) ≈ 409
4343
locktime: 0,
4444
INVALID_CHAIN_ID: 'wrong chain id',
4545
VALID_C_CHAIN_ID: 'yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp',

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,71 @@ describe('Flrp Import In C Tx Builder', () => {
5252
txHash: testData.txhash,
5353
});
5454

55+
describe('dynamic fee calculation', () => {
56+
it('should calculate proper fee using feeRate multiplier (AVAXP approach) to avoid "insufficient unlocked funds" error', async () => {
57+
const amount = '100000000'; // 100M nanoFLRP (0.1 FLR)
58+
const feeRate = '1'; // 1 nanoFLRP per cost unit (matching AVAXP's feeRate usage)
59+
60+
const utxo: DecodedUtxoObj = {
61+
outputID: 0,
62+
amount: amount,
63+
txid: '2vPMx8P63adgBae7GAWFx7qvJDwRmMnDCyKddHRBXWhysjX4BP',
64+
outputidx: '0',
65+
addresses: [
66+
'0x3329be7d01cd3ebaae6654d7327dd9f17a2e1581',
67+
'0x7e918a5e8083ae4c9f2f0ed77055c24bf3665001',
68+
'0xc7324437c96c7c8a6a152da2385c1db5c3ab1f91',
69+
],
70+
threshold: 2,
71+
};
72+
73+
const txBuilder = factory
74+
.getImportInCBuilder()
75+
.threshold(2)
76+
.fromPubKey(testData.pAddresses)
77+
.utxos([utxo])
78+
.to(testData.to)
79+
.feeRate(feeRate) as any;
80+
81+
const tx = await txBuilder.build();
82+
83+
const calculatedFee = BigInt((tx as any).fee.fee);
84+
const feeRateBigInt = BigInt(feeRate);
85+
86+
// The fee should be approximately: feeRate × (txSize + inputCost + fixedFee)
87+
// For 1 input, threshold=2, ~228 bytes: 1 × (228 + 2000 + 10000) = 12,228
88+
const expectedMinCost = 12000; // Minimum cost units (conservative estimate)
89+
const expectedMaxCost = 13000; // Maximum cost units (with some buffer)
90+
91+
const expectedMinFee = feeRateBigInt * BigInt(expectedMinCost);
92+
const expectedMaxFee = feeRateBigInt * BigInt(expectedMaxCost);
93+
94+
// Verify fee is in the expected range
95+
assert(
96+
calculatedFee >= expectedMinFee,
97+
`Fee ${calculatedFee} should be at least ${expectedMinFee} (feeRate × minCost)`
98+
);
99+
assert(
100+
calculatedFee <= expectedMaxFee,
101+
`Fee ${calculatedFee} should not exceed ${expectedMaxFee} (feeRate × maxCost)`
102+
);
103+
104+
// Verify the output amount is positive (no "insufficient funds" error)
105+
const outputs = tx.outputs;
106+
outputs.length.should.equal(1);
107+
const outputAmount = BigInt(outputs[0].value);
108+
assert(
109+
outputAmount > BigInt(0),
110+
'Output amount should be positive - transaction should not fail with insufficient funds'
111+
);
112+
113+
// Verify the math: input - output = fee
114+
const inputAmount = BigInt(amount);
115+
const calculatedOutput = inputAmount - calculatedFee;
116+
assert(outputAmount === calculatedOutput, 'Output should equal input minus total fee');
117+
});
118+
});
119+
55120
describe('on-chain verified transactions', () => {
56121
it('should verify on-chain tx id for signed C-chain import', async () => {
57122
const signedImportHex =

0 commit comments

Comments
 (0)