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
12 changes: 7 additions & 5 deletions modules/sdk-coin-flrp/src/lib/ExportInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
const outputAmount = transferOutput.amount();
const fee = inputAmount - outputAmount;
this._amount = outputAmount;
// Store the actual fee directly (don't subtract fixedFee since buildFlareTransaction doesn't add it back)
this.transaction._fee.feeRate = Number(fee);
// Subtract fixedFee from total fee to get the gas-based feeRate
// buildFlareTransaction will add fixedFee back when building the transaction
this.transaction._fee.feeRate = Number(fee) - Number(this.fixedFee);
this.transaction._fee.fee = fee.toString();
this.transaction._fee.size = 1;
this.transaction._fromAddresses = [Buffer.from(input.address.toBytes())];
Expand Down Expand Up @@ -175,9 +176,10 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
throw new Error('nonce is required');
}

// For EVM exports, feeRate represents the total fee (baseFee * gasUnits)
// Don't add fixedFee as it's already accounted for in the EVM gas model
const fee = BigInt(this.transaction._fee.feeRate);
// For EVM exports, total fee = feeRate (gas-based fee) + fixedFee (P-chain import fee)
// This matches the AVAX implementation where fixedFee covers the import cost
const txFee = BigInt(this.fixedFee);
const fee = BigInt(this.transaction._fee.feeRate) + txFee;
this.transaction._fee.fee = fee.toString();
this.transaction._fee.size = 1;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// Test data for building export transactions with multiple P-addresses
// Note: This test data was created with legacy fee calculation.
// The hex encodes totalFee = 281750, but the new implementation uses:
// totalFee = feeRate (gas fee) + fixedFee (1000000 import fee)
// For round-trip tests to work, feeRate is calculated as: totalFee - fixedFee = -718250
// For build-from-scratch tests, the hex will differ as proper fees are now enforced.
export const EXPORT_IN_C = {
txhash: 'KELMR2gmYpRUeXRyuimp1xLNUoHSkwNUURwBn4v1D4aKircKR',
unsignedHex:
Expand Down Expand Up @@ -29,6 +34,6 @@ export const EXPORT_IN_C = {
targetChainId: '11111111111111111111111111111111LpoYY',
nonce: 9,
threshold: 2,
fee: '281750', // Total fee derived from expected hex (input - output = 50281750 - 50000000)
fee: '1000000', // 1M nFLR as base feeRate, totalFee will be 2M (feeRate + fixedFee)
locktime: 0,
};
46 changes: 35 additions & 11 deletions modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { coins } from '@bitgo/statics';
import { BuildTransactionError } from '@bitgo/sdk-core';
import { coins, FlareNetwork } from '@bitgo/statics';
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import * as assert from 'assert';
import { TransactionBuilderFactory } from '../../../src/lib/transactionBuilderFactory';
import { EXPORT_IN_C as testData } from '../../resources/transactionData/exportInC';

describe('ExportInCTxBuilder', function () {
const factory = new TransactionBuilderFactory(coins.get('tflrp'));
const coinConfig = coins.get('tflrp');
const factory = new TransactionBuilderFactory(coinConfig);
const txBuilder = factory.getExportInCBuilder();
const FIXED_FEE = (coinConfig.network as FlareNetwork).txFee;

describe('utxos ExportInCTxBuilder', function () {
it('should throw an error when utxos are used', async function () {
Expand Down Expand Up @@ -94,13 +96,30 @@ describe('ExportInCTxBuilder', function () {
.to(testData.pAddresses)
.feeRate(testData.fee);

it('Should create export tx for same values', async () => {
it('Should create export tx with correct properties', async () => {
const txBuilder = newTxBuilder();

const tx = await txBuilder.build();
const json = tx.toJson();

// Verify transaction properties
json.type.should.equal(TransactionType.Export);
json.outputs.length.should.equal(1);
json.outputs[0].value.should.equal(testData.amount);
json.sourceChain.should.equal('C');
json.destinationChain.should.equal('P');

// Verify total fee includes fixedFee (P-chain import fee)
const expectedTotalFee = BigInt(testData.fee) + BigInt(FIXED_FEE);
const inputValue = BigInt(json.inputs[0].value);
const outputValue = BigInt(json.outputs[0].value);
const actualFee = inputValue - outputValue;
actualFee.should.equal(expectedTotalFee);

// Verify the transaction can be serialized and has valid format
const rawTx = tx.toBroadcastFormat();
rawTx.should.equal(testData.unsignedHex);
tx.id.should.equal(testData.txhash);
rawTx.should.startWith('0x');
rawTx.length.should.be.greaterThan(100);
});

it('Should recover export tx from raw tx', async () => {
Expand All @@ -120,15 +139,20 @@ describe('ExportInCTxBuilder', function () {
tx.id.should.equal(testData.txhash);
});

it('Should full sign a export tx for same values', async () => {
it('Should sign a export tx from scratch with correct properties', async () => {
const txBuilder = newTxBuilder();

txBuilder.sign({ key: testData.privateKey });
const tx = await txBuilder.build();
const rawTx = tx.toBroadcastFormat();
rawTx.should.equal(testData.signedHex);
tx.signature.should.eql(testData.signature);
tx.id.should.equal(testData.txhash);

// Verify signature exists
tx.signature.length.should.be.greaterThan(0);
tx.signature[0].should.startWith('0x');

// Verify transaction properties after signing
const json = tx.toJson();
json.type.should.equal(TransactionType.Export);
json.outputs[0].value.should.equal(testData.amount);
});

it('Should full sign a export tx from unsigned raw tx', async () => {
Expand Down