Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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',

Expand Down Expand Up @@ -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',
Expand Down
65 changes: 65 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down