Skip to content

Commit 542a8d5

Browse files
committed
refactor(sdk-coin-flrp): enhance transaction builders with improved fee state management and amount handling
TICKET: WIN-8498
1 parent 592c647 commit 542a8d5

8 files changed

Lines changed: 107 additions & 142 deletions

File tree

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

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,27 @@ import {
88
Address,
99
TransferOutput,
1010
utils as FlareUtils,
11-
Utxo,
1211
evm,
1312
} from '@flarenetwork/flarejs';
1413
import utils from './utils';
15-
import { Tx, FlareTransactionType, Context, ExportEVMOptions } from './iface';
14+
import { Tx, FlareTransactionType, ExportEVMOptions } from './iface';
1615

1716
export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
18-
private _amount: bigint;
1917
private _nonce: bigint;
20-
private _context: Context;
2118

2219
constructor(_coinConfig: Readonly<CoinConfig>) {
2320
super(_coinConfig);
2421
}
2522

2623
/**
27-
* Utxos are not required in Export Tx in C-Chain.
28-
* Override utxos to prevent used by throwing a error.
24+
* UTXOs are not required for Export Tx from C-Chain (uses EVM balance instead).
25+
* Override to prevent usage by throwing an error.
2926
*
30-
* @param {Utxo[]} utxos ignored
27+
* @param {string[]} _utxoHexStrings - ignored, UTXOs not used for C-chain exports
28+
* @throws {BuildTransactionError} always throws as UTXOs are not applicable
3129
*/
32-
utxos(utxos: Utxo[]): this {
33-
throw new BuildTransactionError('utxos are not required in Export Tx in C-Chain');
34-
}
35-
36-
/**
37-
* Amount is a bigint that specifies the quantity of the asset that this output owns. Must be positive.
38-
* The transaction output amount add a fixed fee that will be paid upon import.
39-
*
40-
* @param {bigint | string} amount The withdrawal amount
41-
*/
42-
amount(amount: bigint | string): this {
43-
const amountBigInt = typeof amount === 'string' ? BigInt(amount) : amount;
44-
this.validateAmount(amountBigInt);
45-
this._amount = amountBigInt;
46-
return this;
30+
utxos(_utxoHexStrings: string[]): this {
31+
throw new BuildTransactionError('UTXOs are not required for Export Tx from C-Chain');
4732
}
4833

4934
/**
@@ -58,11 +43,6 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
5843
return this;
5944
}
6045

61-
context(context: Context): this {
62-
this._context = context;
63-
return this;
64-
}
65-
6646
/**
6747
* Export tx target P wallet.
6848
*
@@ -106,7 +86,7 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
10686
const inputAmount = input.amount.value();
10787
const outputAmount = transferOutput.amount();
10888
const fee = inputAmount - outputAmount;
109-
this._amount = outputAmount;
89+
this.transaction._amount = outputAmount;
11090
this.transaction._fee.fee = fee.toString();
11191
this.transaction._fromAddresses = [Buffer.from(input.address.toBytes())];
11292
this.transaction._locktime = transferOutput.getLocktime();
@@ -149,23 +129,23 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
149129
*/
150130
protected buildFlareTransaction(): void {
151131
if (this.transaction.hasCredentials) return;
152-
if (this._amount === undefined) {
153-
throw new Error('amount is required');
132+
if (this.transaction._amount === undefined) {
133+
throw new BuildTransactionError('amount is required');
154134
}
155135
if (this.transaction._fromAddresses.length !== 1) {
156-
throw new Error('sender is one and required');
136+
throw new BuildTransactionError('sender is one and required');
157137
}
158138
if (this.transaction._to.length === 0) {
159-
throw new Error('to is required');
139+
throw new BuildTransactionError('to is required');
160140
}
161141
if (!this.transaction._fee.fee) {
162-
throw new Error('fee rate is required');
142+
throw new BuildTransactionError('fee rate is required');
163143
}
164144
if (this._nonce === undefined) {
165-
throw new Error('nonce is required');
145+
throw new BuildTransactionError('nonce is required');
166146
}
167-
if (!this._context) {
168-
throw new Error('context is required');
147+
if (!this.transaction._context) {
148+
throw new BuildTransactionError('context is required');
169149
}
170150

171151
const fee = BigInt(this.transaction._fee.fee);
@@ -183,10 +163,10 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
183163
};
184164

185165
const exportTx = evm.newExportTxFromBaseFee(
186-
this._context,
166+
this.transaction._context,
187167
fee / BigInt(1e9),
188-
this._amount,
189-
this._context.pBlockchainID,
168+
this.transaction._amount,
169+
this.transaction._context.pBlockchainID,
190170
fromAddressBytes,
191171
toAddresses.map((addr) => Buffer.from(addr.toBytes())),
192172
BigInt(this._nonce),

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

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,9 @@ import {
1313
pvm,
1414
} from '@flarenetwork/flarejs';
1515
import utils from './utils';
16-
import { DecodedUtxoObj, SECP256K1_Transfer_Output, FlareTransactionType, Tx, Context } from './iface';
17-
import { FlrpFeeState } from '@bitgo/public-types';
16+
import { DecodedUtxoObj, SECP256K1_Transfer_Output, FlareTransactionType, Tx } from './iface';
1817

1918
export class ExportInPTxBuilder extends AtomicTransactionBuilder {
20-
private _amount: bigint;
21-
private _feeState: FlrpFeeState;
22-
private _context: Context;
23-
2419
constructor(_coinConfig: Readonly<CoinConfig>) {
2520
super(_coinConfig);
2621
// For Export FROM P-chain:
@@ -37,41 +32,6 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
3732
return TransactionType.Export;
3833
}
3934

40-
/**
41-
* Amount is a bigint that specifies the quantity of the asset that this output owns. Must be positive.
42-
* @param {bigint | string} amount The withdrawal amount
43-
*/
44-
amount(value: bigint | string): this {
45-
const valueBigInt = typeof value === 'string' ? BigInt(value) : value;
46-
this.validateAmount(valueBigInt);
47-
this._amount = valueBigInt;
48-
return this;
49-
}
50-
51-
feeState(state: FlrpFeeState): this {
52-
this._feeState = state;
53-
return this;
54-
}
55-
56-
context(context: Context): this {
57-
this._context = context;
58-
return this;
59-
}
60-
61-
/**
62-
* Sets the UTXOs for the transaction from hex strings
63-
* @param utxoHexStrings - Array of UTXO hex strings from getUTXOs API
64-
*/
65-
utxos(utxoHexStrings: string[]): this {
66-
if (!utxoHexStrings || utxoHexStrings.length === 0) {
67-
throw new BuildTransactionError('UTXOs array cannot be empty');
68-
}
69-
this.transaction._utxoHexStrings = utxoHexStrings;
70-
const nativeUtxos = utils.parseUtxoHexArray(utxoHexStrings);
71-
this.transaction._utxos = utils.utxosToDecoded(nativeUtxos, this.transaction._network);
72-
return this;
73-
}
74-
7535
initBuilder(tx: Tx, rawBytes?: Buffer, parsedCredentials?: Credential[]): this {
7636
const exportTx = tx as pvmSerial.ExportTx;
7737

@@ -95,15 +55,15 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
9555
this.transaction._threshold = outputOwners.threshold.value();
9656
this.transaction._fromAddresses = outputOwners.addrs.map((addr) => Buffer.from(addr.toBytes()));
9757
this._externalChainId = Buffer.from(exportTx.destination.toBytes());
98-
this._amount = outputTransfer.amount();
58+
this.transaction._amount = outputTransfer.amount();
9959
this.transaction._utxos = this.recoverUtxos([...exportTx.baseTx.inputs]);
10060

10161
const totalInputAmount = exportTx.baseTx.inputs.reduce((sum, input) => sum + input.amount(), BigInt(0));
10262
const changeOutputAmount = exportTx.baseTx.outputs.reduce((sum, out) => {
10363
const transferOut = out.output as TransferOutput;
10464
return sum + transferOut.amount();
10565
}, BigInt(0));
106-
const fee = totalInputAmount - changeOutputAmount - this._amount;
66+
const fee = totalInputAmount - changeOutputAmount - this.transaction._amount;
10767
this.transaction._fee.fee = fee.toString();
10868

10969
const credentials = parsedCredentials || [];
@@ -157,25 +117,35 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
157117
protected async buildFlareTransaction(): Promise<void> {
158118
if (this.transaction.hasCredentials) return;
159119

160-
const feeState = this._feeState;
120+
const feeState = this.transaction._feeState;
161121
if (!feeState) {
162122
throw new BuildTransactionError('Fee state is required');
163123
}
164-
if (!this._context) {
124+
if (!this.transaction._context) {
165125
throw new BuildTransactionError('context is required');
166126
}
167-
if (this._amount === undefined) {
127+
if (this.transaction._amount === undefined) {
168128
throw new BuildTransactionError('amount is required');
169129
}
170130

171131
const nativeUtxos = utils.parseUtxoHexArray(this.transaction._utxoHexStrings);
172132

133+
const totalUtxoAmount = nativeUtxos.reduce((sum, utxo) => {
134+
const output = utxo.output as TransferOutput;
135+
return sum + output.amount();
136+
}, BigInt(0));
137+
138+
if (totalUtxoAmount < this.transaction._amount) {
139+
throw new BuildTransactionError(
140+
`Insufficient UTXO balance: have ${totalUtxoAmount.toString()} nFLR, need at least ${this.transaction._amount.toString()} nFLR (plus fee)`
141+
);
142+
}
143+
173144
const assetId = utils.flareIdString(this.transaction._assetId).toString();
174-
``;
175145
const fromAddresses = this.transaction._fromAddresses.map((addr) => Buffer.from(addr));
176146
const transferableOutput = TransferableOutput.fromNative(
177147
assetId,
178-
this._amount,
148+
this.transaction._amount,
179149
fromAddresses,
180150
this.transaction._locktime,
181151
this.transaction._threshold
@@ -189,7 +159,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
189159
outputs: [transferableOutput],
190160
utxos: nativeUtxos,
191161
},
192-
this._context
162+
this.transaction._context
193163
);
194164

195165
this.transaction.setTransaction(exportTx);

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,19 @@ import {
66
UnsignedTx,
77
Credential,
88
TransferableInput,
9+
TransferOutput,
910
Address,
1011
utils as FlareUtils,
1112
evm,
1213
} from '@flarenetwork/flarejs';
1314
import utils from './utils';
14-
import { Context, DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface';
15+
import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface';
1516

1617
export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
17-
private _context: Context;
18-
1918
constructor(_coinConfig: Readonly<CoinConfig>) {
2019
super(_coinConfig);
2120
}
2221

23-
context(context: Context): this {
24-
this._context = context;
25-
return this;
26-
}
27-
2822
/**
2923
* C-chain address who is target of the import.
3024
* Address format is eth like
@@ -135,13 +129,13 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
135129
protected buildFlareTransaction(): void {
136130
if (this.transaction.hasCredentials) return;
137131
if (this.transaction._to.length !== 1) {
138-
throw new Error('to is required');
132+
throw new BuildTransactionError('to is required');
139133
}
140134
if (!this.transaction._fee.fee) {
141-
throw new Error('fee is required');
135+
throw new BuildTransactionError('fee is required');
142136
}
143-
if (!this._context) {
144-
throw new Error('context is required');
137+
if (!this.transaction._context) {
138+
throw new BuildTransactionError('context is required');
145139
}
146140
if (!this.transaction._fromAddresses || this.transaction._fromAddresses.length === 0) {
147141
throw new BuildTransactionError('fromAddresses are required');
@@ -153,18 +147,31 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
153147
throw new BuildTransactionError('threshold is required');
154148
}
155149

156-
const estimatedGasUnits = 200000n;
157-
const baseFeeNFlr = BigInt(this._transaction._fee.fee) / BigInt(1_000_000_000);
158-
const actualFee = baseFeeNFlr * estimatedGasUnits;
150+
const estimatedGasUnits = BigInt(this.transaction._network.txFee) || 200000n;
151+
const baseFeeInWei = BigInt(this.transaction._fee.fee);
152+
const baseFeeGwei = baseFeeInWei / BigInt(1e9);
153+
const actualFeeNFlr = baseFeeGwei * estimatedGasUnits;
159154
const sourceChain = 'P';
160155
const nativeUtxos = utils.parseUtxoHexArray(this.transaction._utxoHexStrings);
156+
157+
// Validate UTXO balance is sufficient to cover the import fee
158+
const totalUtxoAmount = nativeUtxos.reduce((sum, utxo) => {
159+
const output = utxo.output as TransferOutput;
160+
return sum + output.amount();
161+
}, BigInt(0));
162+
163+
if (totalUtxoAmount <= actualFeeNFlr) {
164+
throw new BuildTransactionError(
165+
`Insufficient UTXO balance: have ${totalUtxoAmount.toString()} nFLR, need more than ${actualFeeNFlr.toString()} nFLR to cover import fee`
166+
);
167+
}
161168
const importTx = evm.newImportTx(
162-
this._context,
169+
this.transaction._context,
163170
this.transaction._to[0],
164171
this.transaction._fromAddresses.map((addr) => Buffer.from(addr)),
165172
nativeUtxos,
166173
sourceChain,
167-
actualFee
174+
actualFeeNFlr
168175
);
169176

170177
this.transaction.setTransaction(importTx);

0 commit comments

Comments
 (0)