Skip to content

Commit 20af49e

Browse files
Merge pull request #7828 from BitGo/WIN-8488-2
refactor(sdk-coin-flrp): enhance fee calculation for import transactions
2 parents 87bfa0a + 7807e8c commit 20af49e

File tree

3 files changed

+161
-168
lines changed

3 files changed

+161
-168
lines changed

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

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
TransferableInput,
1212
Address,
1313
utils as FlareUtils,
14-
avmSerial,
1514
} from '@flarenetwork/flarejs';
1615
import utils from './utils';
1716
import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface';
@@ -63,16 +62,6 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
6362

6463
// Calculate fee based on input/output difference
6564
const fee = totalInputAmount - totalOutputAmount;
66-
// Calculate cost units using the same method as buildFlareTransaction
67-
const feeSize = this.calculateImportCost(baseTx);
68-
// Use integer division to ensure feeRate can be converted back to BigInt
69-
const feeRate = Math.floor(Number(fee) / feeSize);
70-
71-
this.transaction._fee = {
72-
fee: fee.toString(),
73-
feeRate: feeRate,
74-
size: feeSize,
75-
};
7665

7766
// Use credentials passed from TransactionBuilderFactory (properly extracted using codec)
7867
const credentials = parsedCredentials || [];
@@ -88,6 +77,30 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
8877
const inputThreshold = firstInput.sigIndicies().length || this.transaction._threshold;
8978
this.transaction._threshold = inputThreshold;
9079

80+
// Create a temporary UnsignedTx for accurate fee size calculation
81+
// This includes the full structure (ImportTx, AddressMaps, Credentials)
82+
const tempAddressMap = new FlareUtils.AddressMap();
83+
for (let i = 0; i < inputThreshold; i++) {
84+
if (this.transaction._fromAddresses && this.transaction._fromAddresses[i]) {
85+
tempAddressMap.set(new Address(this.transaction._fromAddresses[i]), i);
86+
}
87+
}
88+
const tempAddressMaps = new FlareUtils.AddressMaps([tempAddressMap]);
89+
const tempCredentials =
90+
credentials.length > 0 ? credentials : [new Credential(Array(inputThreshold).fill(utils.createNewSig('')))];
91+
const tempUnsignedTx = new UnsignedTx(baseTx, [], tempAddressMaps, tempCredentials);
92+
93+
// Calculate cost units using the full UnsignedTx structure
94+
const feeSize = this.calculateImportCost(tempUnsignedTx);
95+
// Use integer division to ensure feeRate can be converted back to BigInt
96+
const feeRate = Math.floor(Number(fee) / feeSize);
97+
98+
this.transaction._fee = {
99+
fee: fee.toString(),
100+
feeRate: feeRate,
101+
size: feeSize,
102+
};
103+
91104
// Create AddressMaps based on signature slot order (matching credential order), not sorted addresses
92105
// This matches the approach used in credentials: addressesIndex determines signature order
93106
// AddressMaps should map addresses to signature slots in the same order as credentials
@@ -166,7 +179,8 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
166179
const { inputs, amount, credentials } = this.createInputs();
167180

168181
// Calculate import cost units (matching AVAXP's costImportTx approach)
169-
// Create a temporary transaction with full amount to calculate fee size
182+
// Create a temporary UnsignedTx with full amount to calculate fee size
183+
// This includes the full structure (ImportTx, AddressMaps, Credentials) for accurate size calculation
170184
const tempOutput = new evmSerial.Output(
171185
new Address(this.transaction._to[0]),
172186
new BigIntPr(amount),
@@ -180,8 +194,18 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
180194
[tempOutput]
181195
);
182196

183-
// Calculate feeSize once using full amount (matching AVAXP approach)
184-
const feeSize = this.calculateImportCost(tempImportTx);
197+
// Create AddressMaps for fee calculation (same as final transaction)
198+
const firstUtxo = this.transaction._utxos[0];
199+
const tempAddressMap = firstUtxo
200+
? this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold)
201+
: new FlareUtils.AddressMap();
202+
const tempAddressMaps = new FlareUtils.AddressMaps([tempAddressMap]);
203+
204+
// Create temporary UnsignedTx with full structure for accurate fee calculation
205+
const tempUnsignedTx = new UnsignedTx(tempImportTx, [], tempAddressMaps, credentials);
206+
207+
// Calculate feeSize once using full UnsignedTx (matching AVAXP approach)
208+
const feeSize = this.calculateImportCost(tempUnsignedTx);
185209
const feeRate = BigInt(this.transaction._fee.feeRate);
186210
const fee = feeRate * BigInt(feeSize);
187211

@@ -211,21 +235,11 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
211235
[output]
212236
);
213237

214-
// Create AddressMaps based on signature slot order (matching credential order), not sorted addresses
215-
// This matches the approach used in credentials: addressesIndex determines signature order
216-
// AddressMaps should map addresses to signature slots in the same order as credentials
217-
// For C-chain imports, we typically have one input, so use the first UTXO
218-
// Use centralized method for AddressMap creation
219-
const firstUtxo = this.transaction._utxos[0];
220-
const addressMap = firstUtxo
221-
? this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold)
222-
: new FlareUtils.AddressMap();
223-
const addressMaps = new FlareUtils.AddressMaps([addressMap]);
224-
238+
// Reuse the AddressMaps already calculated for fee calculation
225239
const unsignedTx = new UnsignedTx(
226240
importTx,
227241
[], // Empty UTXOs array, will be filled during processing
228-
addressMaps,
242+
tempAddressMaps,
229243
credentials
230244
);
231245

@@ -298,39 +312,21 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
298312
}
299313

300314
/**
301-
* Calculate the import cost for C-chain import transactions
302-
* Matches AVAXP's costImportTx formula:
303-
* - Base byte cost: transactionSize * txBytesGas (1 gas per byte)
304-
* - Per-input cost: numInputs * costPerSignature (1000 per signature) * threshold
305-
* - Fixed fee: 10000
306-
*
307-
* This returns cost "units" to be multiplied by feeRate, matching AVAXP's approach:
308-
* AVAXP: fee = feeRate.muln(costImportTx(tx))
309-
* FLRP: fee = feeRate * calculateImportCost(tx)
310-
*
311-
* @param tx The ImportTx to calculate the cost for
315+
* @param unsignedTx The UnsignedTx to calculate the cost for (includes ImportTx, AddressMaps, and Credentials)
312316
* @returns The total cost units
313317
*/
314-
private calculateImportCost(tx: evmSerial.ImportTx): number {
315-
const codec = avmSerial.getAVMManager().getDefaultCodec();
316-
const txBytes = tx.toBytes(codec);
317-
318-
// Base byte cost: 1 gas per byte (matching AVAX txBytesGas)
318+
private calculateImportCost(unsignedTx: UnsignedTx): number {
319+
const signedTxBytes = unsignedTx.getSignedTx().toBytes();
319320
const txBytesGas = 1;
320-
let bytesCost = txBytes.length * txBytesGas;
321-
322-
// Per-input cost: costPerSignature (1000) per signature
321+
let bytesCost = signedTxBytes.length * txBytesGas;
323322
const costPerSignature = 1000;
324-
const numInputs = tx.importedInputs.length;
325-
const numSignatures = this.transaction._threshold; // Each input requires threshold signatures
326-
const inputCost = numInputs * costPerSignature * numSignatures;
327-
bytesCost += inputCost;
328-
329-
// Fixed fee component
323+
const importTx = unsignedTx.getTx() as evmSerial.ImportTx;
324+
importTx.importedInputs.forEach((input: TransferableInput) => {
325+
const inCost = costPerSignature * input.sigIndicies().length;
326+
bytesCost += inCost;
327+
});
330328
const fixedFee = 10000;
331-
const totalCost = bytesCost + fixedFee;
332-
333-
return totalCost;
329+
return bytesCost + fixedFee;
334330
}
335331

336332
/**

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: '2Q5RkxF2eRK3KCzDaijoScyunahbEvt6ai6YZipmShQTPryfky',
2+
txhash: '5wgxtB8tSyS2MNroAQgs8fA9sDUWCwrGJG88b77xUm1po685Q',
33
unsignedHex:
4-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e15810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576b6e21',
4+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000100000009000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003329be7d01cd3ebaae6654d7327dd9f17a2e158100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ea9607a',
55
halfSignedSignature:
66
'0xd365ef7ce45aebc4e81bc03f600867f515cebb25c4a0e8e1f06d9fe0a00d41fd2efac6c6df392e5f92e271c57486e39425537da7cafbb085cd1bd21aff06955d00',
77
halfSigntxHex:
8-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000077fae449',
8+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002e0c6d38e404ebeb08417a49c6c227e8c6ec5fd0c6a49d74eafa280cb8c6e19fe4d230834a0a3af5c3a49f3aefa9fc5f3d1be234aa8625f878634c9c079e58086010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000196ceb32',
99
fullSigntxHex:
10-
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d8114dc58734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002decd468a395c16b7bc799d387196848aec99602b00fe8cdc2d9ed55aaf373db13aa33444c9e43a8707a75ece2dc7081c628422b6b137f7c11f428b99c48b1db901d833ae918ca0bc59a4495e98837ffca0870666aaea0fbb8fd9b510e21e24f81071c2a622cd8979138e65ae413a0b1b573e2615dba04778a44f2b6c72566dd13401d6bc2f0a',
10+
'0x0000000000000000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da5552479000000000000000000000000000000000000000000000000000000000000000000000001fcea1c0e2cb7e3d77c993eb74ee05d98c24325ded1918e8a0595c96a789e2f790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd00000005000000001dcd65000000000200000000000000010000000117dbd11b9dd1c9be337353db7c14f9fb3662e5b5000000001d6587f058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000010000000900000002e0c6d38e404ebeb08417a49c6c227e8c6ec5fd0c6a49d74eafa280cb8c6e19fe4d230834a0a3af5c3a49f3aefa9fc5f3d1be234aa8625f878634c9c079e5808601ecb60f57234c5e852add6fbbf90f2c8613b4b1a520aefe6f7e78b5e21155d1f131107cf9177764c8dda5936e1dd8d5846ea9b7c016e3811565ab8b48776a15a100bb68cc63',
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: '409', // feeRate multiplier: 5,000,000 (desired fee) ÷ ~12,228 (cost units) ≈ 409
42+
fee: '550',
4343
locktime: 0,
4444
INVALID_CHAIN_ID: 'wrong chain id',
4545
VALID_C_CHAIN_ID: 'yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp',

0 commit comments

Comments
 (0)