diff --git a/modules/sdk-coin-avaxp/test/unit/lib/exportC2PTxBuilder.ts b/modules/sdk-coin-avaxp/test/unit/lib/exportC2PTxBuilder.ts index e78516570e..ddd25ca4ff 100644 --- a/modules/sdk-coin-avaxp/test/unit/lib/exportC2PTxBuilder.ts +++ b/modules/sdk-coin-avaxp/test/unit/lib/exportC2PTxBuilder.ts @@ -34,7 +34,7 @@ describe('AvaxP Export C2P Tx Builder', () => { }); }); - describe('should build ', () => { + describe('should build C to P transaction', () => { const newTxBuilder = () => factory .getExportInCBuilder() diff --git a/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts index 7f26bd836d..2e71c3677f 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts @@ -2,12 +2,13 @@ import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import utils from './utils'; import { BuildTransactionError } from '@bitgo/sdk-core'; +import { Transaction } from './transaction'; -interface FlareChainNetworkMeta { - blockchainID?: string; // P-chain id (external) - cChainBlockchainID?: string; // C-chain id (local) - [k: string]: unknown; -} +// interface FlareChainNetworkMeta { +// blockchainID?: string; // P-chain id (external) +// cChainBlockchainID?: string; // C-chain id (local) +// [k: string]: unknown; +// } interface FeeShape { fee?: string; // legacy @@ -41,12 +42,12 @@ export abstract class AtomicInCTransactionBuilder extends AtomicTransactionBuild /** * Recreate builder state from raw tx (hex). Flare C-chain support TBD; for now validate & stash. */ - protected fromImplementation(rawTransaction: string): { _tx?: unknown } { + protected fromImplementation(rawTransaction: string): Transaction { // If utils has validateRawTransaction use it; otherwise basic check if ((utils as unknown as { validateRawTransaction?: (r: string) => void }).validateRawTransaction) { (utils as unknown as { validateRawTransaction: (r: string) => void }).validateRawTransaction(rawTransaction); } - this.transaction.setTransaction(rawTransaction); + // this.transaction.setTransaction(rawTransaction); return this.transaction; } @@ -57,13 +58,8 @@ export abstract class AtomicInCTransactionBuilder extends AtomicTransactionBuild } private initializeChainIds(): void { - const meta = this.transaction._network as FlareChainNetworkMeta; - if (meta?.blockchainID) { - this._externalChainId = utils.cb58Decode(meta.blockchainID); - } - if (meta?.cChainBlockchainID) { - this.transaction._blockchainID = utils.cb58Decode(meta.cChainBlockchainID); - } + this._externalChainId = utils.cb58Decode(this.transaction._network.blockchainID); + this._blockchainID = utils.cb58Decode(this.transaction._network.cChainBlockchainID); } private setFeeRate(n: bigint): void { diff --git a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts index 97abda8650..dd31e83e0a 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts @@ -1,22 +1,19 @@ -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType, BaseTransaction } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig, FlareNetwork } from '@bitgo/statics'; +import { BuildTransactionError, TransactionType, BaseTransaction, BaseKey } from '@bitgo/sdk-core'; import { Credential, Signature, TransferableInput, TransferableOutput } from '@flarenetwork/flarejs'; -import { TransactionExplanation, DecodedUtxoObj } from './iface'; +import { TransactionExplanation, DecodedUtxoObj, BaseAddress } from './iface'; +import { KeyPair } from './keyPair'; +import BigNumber from 'bignumber.js'; import { ASSET_ID_LENGTH, TRANSACTION_ID_HEX_LENGTH, PRIVATE_KEY_HEX_LENGTH, SECP256K1_SIGNATURE_LENGTH, - TRANSACTION_ID_PREFIX, - DEFAULT_NETWORK_ID, - EMPTY_BUFFER_SIZE, HEX_PREFIX, HEX_PREFIX_LENGTH, DECIMAL_RADIX, SIGNING_METHOD, AMOUNT_STRING_ZERO, - DEFAULT_LOCKTIME, - DEFAULT_THRESHOLD, ZERO_BIGINT, ZERO_NUMBER, ERROR_AMOUNT_POSITIVE, @@ -25,61 +22,48 @@ import { ERROR_SIGNATURES_ARRAY, ERROR_SIGNATURES_EMPTY, ERROR_INVALID_PRIVATE_KEY, - ERROR_UTXOS_REQUIRED_BUILD, - ERROR_ENHANCED_BUILD_FAILED, ERROR_ENHANCED_PARSE_FAILED, ERROR_FLAREJS_SIGNING_FAILED, ERROR_CREATE_CREDENTIAL_FAILED, ERROR_UNKNOWN, - FLARE_ATOMIC_PREFIX, FLARE_ATOMIC_PARSED_PREFIX, HEX_ENCODING, } from './constants'; -import { createFlexibleHexRegex } from './utils'; +import utils, { createFlexibleHexRegex } from './utils'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; /** * Flare P-chain atomic transaction builder with FlareJS credential support. * This provides the foundation for building Flare P-chain transactions with proper * credential handling using FlareJS Credential and Signature classes. */ -export abstract class AtomicTransactionBuilder { +export abstract class AtomicTransactionBuilder extends TransactionBuilder { protected readonly _coinConfig: Readonly; // External chain id (destination) for export transactions protected _externalChainId: Buffer | undefined; protected _utxos: DecodedUtxoObj[] = []; - - protected transaction: { - _network: Record; - _networkID: number; - _blockchainID: Buffer; - _assetId: Buffer; - _fromAddresses: string[]; - _to: string[]; - _locktime: bigint; - _threshold: number; - _fee: { fee: string; feeRate?: string; size?: number }; - hasCredentials: boolean; - _tx?: unknown; - _signature?: unknown; - setTransaction: (tx: unknown) => void; - } = { - _network: {}, - _networkID: DEFAULT_NETWORK_ID, - _blockchainID: Buffer.alloc(EMPTY_BUFFER_SIZE), - _assetId: Buffer.alloc(EMPTY_BUFFER_SIZE), - _fromAddresses: [], - _to: [], - _locktime: DEFAULT_LOCKTIME, - _threshold: DEFAULT_THRESHOLD, - _fee: { fee: AMOUNT_STRING_ZERO }, - hasCredentials: false, - setTransaction: function (_tx: unknown) { - this._tx = _tx; - }, - }; + // TODO check _flrpTransaction type + protected _flrpTransaction: any; + + public _network: FlareNetwork; + public _networkID: number; + public _blockchainID: Buffer; + public _assetId: Buffer; + public _fromAddresses: string[]; + public _to: string[]; + public _locktime: bigint; + public _threshold: number; + public _fee: { fee: string; feeRate?: string; size?: number }; + public hasCredentials: boolean; + public _tx?: unknown; + public _signature?: unknown; + public _type: TransactionType; + public _rewardAddresses: Buffer[]; constructor(coinConfig: Readonly) { + super(coinConfig); this._coinConfig = coinConfig; } @@ -91,8 +75,8 @@ export abstract class AtomicTransactionBuilder { */ protected getAssetId(): Buffer { // Use the asset ID from transaction if already set - if (this.transaction._assetId && this.transaction._assetId.length > 0) { - return this.transaction._assetId; + if (this._assetId && this._assetId.length > 0) { + return this._assetId; } // For native FLR transactions, return zero-filled buffer as placeholder @@ -218,7 +202,7 @@ export abstract class AtomicTransactionBuilder { assetID: this.getAssetId(), output: { amount: changeAmount, - addresses: this.transaction._fromAddresses, + addresses: this._fromAddresses, threshold: 1, locktime: 0n, }, @@ -326,8 +310,8 @@ export abstract class AtomicTransactionBuilder { }; // Store signature for FlareJS compatibility - this.transaction._signature = signature; - this.transaction.hasCredentials = true; + this._signature = signature; + this.hasCredentials = true; return this; } catch (error) { @@ -340,62 +324,62 @@ export abstract class AtomicTransactionBuilder { /** * Build the transaction using FlareJS compatibility */ - async build(): Promise { - // FlareJS UnsignedTx creation with atomic transaction support - try { - // Validate transaction requirements - if (!this._utxos || this._utxos.length === 0) { - throw new BuildTransactionError(ERROR_UTXOS_REQUIRED_BUILD); - } - - // Create FlareJS transaction structure with atomic support - const transaction = { - _id: `${TRANSACTION_ID_PREFIX}${Date.now()}`, - _inputs: [], - _outputs: [], - _type: this.transactionType, - signature: [] as string[], - - fromAddresses: this.transaction._fromAddresses, - validationErrors: [], - - // FlareJS methods with atomic support - toBroadcastFormat: () => `${TRANSACTION_ID_PREFIX}${Date.now()}`, - toJson: () => ({ - type: this.transactionType, - }), - - explainTransaction: (): TransactionExplanation => ({ - type: this.transactionType, - inputs: [], - outputs: [], - outputAmount: AMOUNT_STRING_ZERO, - rewardAddresses: [], - id: `${FLARE_ATOMIC_PREFIX}${Date.now()}`, - changeOutputs: [], - changeAmount: AMOUNT_STRING_ZERO, - fee: { fee: this.transaction._fee.fee }, - }), - - isTransactionForCChain: false, - loadInputsAndOutputs: () => { - /* FlareJS atomic transaction loading */ - }, - inputs: () => [], - outputs: () => [], - fee: () => ({ fee: this.transaction._fee.fee }), - feeRate: () => 0, - id: () => `${FLARE_ATOMIC_PREFIX}${Date.now()}`, - type: this.transactionType, - } as unknown as BaseTransaction; - - return transaction; - } catch (error) { - throw new BuildTransactionError( - `${ERROR_ENHANCED_BUILD_FAILED}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` - ); - } - } + // async build(): Promise { + // // FlareJS UnsignedTx creation with atomic transaction support + // try { + // // Validate transaction requirements + // if (!this._utxos || this._utxos.length === 0) { + // throw new BuildTransactionError(ERROR_UTXOS_REQUIRED_BUILD); + // } + + // // Create FlareJS transaction structure with atomic support + // const transaction = { + // _id: `${TRANSACTION_ID_PREFIX}${Date.now()}`, + // _inputs: [], + // _outputs: [], + // _type: this.transactionType, + // signature: [] as string[], + + // fromAddresses: this._fromAddresses, + // validationErrors: [], + + // // FlareJS methods with atomic support + // toBroadcastFormat: () => `${TRANSACTION_ID_PREFIX}${Date.now()}`, + // toJson: () => ({ + // type: this.transactionType, + // }), + + // explainTransaction: (): TransactionExplanation => ({ + // type: this.transactionType, + // inputs: [], + // outputs: [], + // outputAmount: AMOUNT_STRING_ZERO, + // rewardAddresses: [], + // id: `${FLARE_ATOMIC_PREFIX}${Date.now()}`, + // changeOutputs: [], + // changeAmount: AMOUNT_STRING_ZERO, + // fee: { fee: this._fee.fee }, + // }), + + // isTransactionForCChain: false, + // loadInputsAndOutputs: () => { + // /* FlareJS atomic transaction loading */ + // }, + // inputs: () => [], + // outputs: () => [], + // fee: () => ({ fee: this._fee.fee }), + // feeRate: () => 0, + // id: () => `${FLARE_ATOMIC_PREFIX}${Date.now()}`, + // type: this.transactionType, + // } as unknown as BaseTransaction; + + // return transaction; + // } catch (error) { + // throw new BuildTransactionError( + // `${ERROR_ENHANCED_BUILD_FAILED}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` + // ); + // } + // } /** * Parse and explain a transaction from hex using FlareJS compatibility @@ -412,7 +396,7 @@ export abstract class AtomicTransactionBuilder { id: `${FLARE_ATOMIC_PARSED_PREFIX}${Date.now()}`, changeOutputs: [], changeAmount: AMOUNT_STRING_ZERO, - fee: { fee: this.transaction._fee.fee }, + fee: { fee: this._fee.fee }, }; } catch (error) { throw new BuildTransactionError( @@ -420,4 +404,72 @@ export abstract class AtomicTransactionBuilder { ); } } + + /** @inheritdoc */ + protected signImplementation({ key }: BaseKey): BaseTransaction { + this._signer.push(new KeyPair({ prv: key })); + return this.transaction; + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + this.buildFlareTransaction(); + this.transaction.setTransactionType(this.transactionType); + if (this.hasSigner) { + this._signer.forEach((keyPair) => this.transaction.sign(keyPair)); + } + return this.transaction; + } + + /** + * Builds the avax transaction. transaction field is changed. + */ + protected abstract buildFlareTransaction(): void; + + /** + * Getter for know if build should sign + */ + get hasSigner(): boolean { + return this._signer !== undefined && this._signer.length > 0; + } + + /** @inheritdoc */ + validateKey({ key }: BaseKey): void { + if (!new KeyPair({ prv: key })) { + throw new BuildTransactionError('Invalid key'); + } + } + + /** @inheritdoc */ + validateAddress(address: BaseAddress, addressFormat?: string): void { + if (!utils.isValidAddress(address.address)) { + throw new BuildTransactionError('Invalid address'); + } + } + + /** @inheritdoc */ + validateValue(value: BigNumber): void { + if (value.isLessThan(0)) { + throw new BuildTransactionError('Value cannot be less than zero'); + } + } + + /** + * Check the raw transaction has a valid format in the blockchain context, throw otherwise. + * It overrides abstract method from BaseTransactionBuilder + * + * @param rawTransaction Transaction in any format + */ + validateRawTransaction(rawTransaction: string): void { + utils.validateRawTransaction(rawTransaction); + } + + /** @inheritdoc */ + validateTransaction(transaction?: Transaction): void { + // throw new NotImplementedError('validateTransaction not implemented'); + } + + // setTransaction(tx: any): void { + // this._flrpTransaction = tx; + // } } diff --git a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts index dddb9fe41a..8fd7b3d375 100644 --- a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts @@ -15,6 +15,7 @@ import { NETWORK_ID_PROP, BLOCKCHAIN_ID_PROP, } from './constants'; +import { Transaction } from './transaction'; // Lightweight interface placeholders replacing Avalanche SDK transaction shapes interface FlareExportInputShape { @@ -111,7 +112,7 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { if (unsigned.networkId !== this.transaction._networkID) { throw new Error('Network ID mismatch'); } - if (!unsigned.sourceBlockchainId.equals(this.transaction._blockchainID)) { + if (!unsigned.sourceBlockchainId.equals(this._blockchainID)) { throw new Error('Blockchain ID mismatch'); } if (unsigned.outputs.length !== 1) { @@ -122,23 +123,23 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { } const out = unsigned.outputs[0]; const inp = unsigned.inputs[0]; - if (!out.assetId.equals(this.transaction._assetId) || !inp.assetId.equals(this.transaction._assetId)) { + if (!out.assetId.equals(this._assetId) || !inp.assetId.equals(this._assetId)) { throw new Error('AssetID mismatch'); } - this.transaction._to = out.addresses; + this._to = out.addresses; this._amount = out.amount; const fee = inp.amount - out.amount; if (fee < 0n) { throw new BuildTransactionError('Computed fee is negative'); } const fixed = this.fixedFee; - const feeRate = fee - fixed; - this.transaction._fee.feeRate = feeRate.toString(); + const feeRate = Number(fee - fixed); + this.transaction._fee.feeRate = feeRate; this.transaction._fee.fee = fee.toString(); this.transaction._fee.size = 1; this.transaction._fromAddresses = [inp.address]; this._nonce = inp.nonce; - this.transaction.setTransaction(raw); + // this.transaction.setTransaction(raw.unsignedTx); return this; } @@ -225,28 +226,32 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { // Create FlareJS-ready unsigned transaction const unsigned: FlareUnsignedExportTx = { networkId: this.transaction._networkID, - sourceBlockchainId: this.transaction._blockchainID, + sourceBlockchainId: this._blockchainID, destinationBlockchainId: this._externalChainId || Buffer.alloc(ASSET_ID_LENGTH), inputs: [input], outputs: [output], }; + console.log('Unsigned Export Tx:', unsigned); + // Create signed transaction structure const signed: FlareSignedExportTx = { unsignedTx: unsigned, credentials: [] }; + console.log('Signed Export Tx:', signed); + // Update transaction fee information this.transaction._fee.fee = totalFee.toString(); this.transaction._fee.size = 1; // Set the enhanced transaction - this.transaction.setTransaction(signed); + // this.transaction.setTransaction(signed); } /** @inheritdoc */ - protected fromImplementation(raw: string | RawFlareExportTx): { _tx?: unknown } { + protected fromImplementation(raw: string | RawFlareExportTx): Transaction { if (typeof raw === STRING_TYPE) { // Future: parse hex or serialized form. For now treat as opaque raw tx. - this.transaction.setTransaction(raw); + // this.transaction.setTransaction(raw); return this.transaction; } return this.initBuilder(raw as RawFlareExportTx).transaction; diff --git a/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts index 194326b9df..663df1526e 100644 --- a/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts @@ -126,7 +126,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { ], // Enhanced fee structure for P-chain operations - fee: BigInt(this.transaction._fee.fee) || BigInt(DEFAULT_BASE_FEE), // Default P-chain fee + fee: BigInt(this._fee.fee) || BigInt(DEFAULT_BASE_FEE), // Default P-chain fee // Credential placeholders ready for FlareJS integration credentials: this.transaction._fromAddresses.map(() => ({ @@ -139,8 +139,10 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder { memo: Buffer.alloc(EMPTY_BUFFER_SIZE), }; + console.log('Enhanced P-chain Export Tx:', enhancedExportTx); + // Store the transaction structure - this.transaction.setTransaction(enhancedExportTx); + // this.transaction.setTransaction(enhancedExportTx); } /** diff --git a/modules/sdk-coin-flrp/src/lib/iface.ts b/modules/sdk-coin-flrp/src/lib/iface.ts index b94370295a..06f61be194 100644 --- a/modules/sdk-coin-flrp/src/lib/iface.ts +++ b/modules/sdk-coin-flrp/src/lib/iface.ts @@ -138,3 +138,7 @@ export interface FlrpSpendOptions { memo?: Uint8Array; // FlareJS memo format (byte array) locktime?: bigint; } + +export interface BaseAddress { + address: string; +} diff --git a/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts index 6688a42423..e62720b5c1 100644 --- a/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts @@ -284,7 +284,8 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { _flareJSReady: true, }; - this.transaction.setTransaction(enhancedTx); + console.log('Enhanced Import Tx:', enhancedTx); + // this.transaction.setTransaction(enhancedTx); } catch (error) { throw new BuildTransactionError(`Failed to build import transaction: ${error}`); } diff --git a/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts index fb0169fa62..3048e0d989 100644 --- a/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts @@ -302,7 +302,8 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { memo: Buffer.alloc(0), }; - this.transaction.setTransaction(enhancedImportTx); + console.log('Enhanced Import Tx:', enhancedImportTx); + // this.transaction.setTransaction(enhancedImportTx); } catch (error) { throw new BuildTransactionError(`Failed to build P-chain import transaction: ${error}`); } @@ -495,14 +496,14 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { return this; } - /** - * Set locktime for the P-chain import transaction - * @param {number | bigint} locktime - Locktime value - */ - locktime(locktime: number | bigint): this { - this.transaction._locktime = typeof locktime === 'number' ? BigInt(locktime) : locktime; - return this; - } + // /** + // * Set locktime for the P-chain import transaction + // * @param {number | bigint} locktime - Locktime value + // */ + // locktime(locktime: number | bigint): this { + // this.transaction._locktime = typeof locktime === 'number' ? BigInt(locktime) : locktime; + // return this; + // } /** * Set threshold for the P-chain import transaction diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts index 73066ffc28..559c64c66c 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts @@ -1,51 +1,11 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { NotImplementedError, TransactionType } from '@bitgo/sdk-core'; import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; - -// Placeholder builders - basic implementations for testing -export class ExportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Export; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class ImportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Import; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class ValidatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddValidator; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class DelegatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddDelegator; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} +import { ImportInCTxBuilder } from './importInCTxBuilder'; +import { ValidatorTxBuilder } from './validatorTxBuilder'; +import { PermissionlessValidatorTxBuilder } from './permissionlessValidatorTxBuilder'; +import { ExportInCTxBuilder } from './exportInCTxBuilder'; +import { ExportInPTxBuilder } from './exportInPTxBuilder'; +import { ImportInPTxBuilder } from './importInPTxBuilder'; /** * Factory for Flare P-chain transaction builders @@ -70,7 +30,7 @@ export class TransactionBuilderFactory { // Create a mock export builder for now // In the future, this will parse the transaction and determine the correct type - const builder = new ExportTxBuilder(this._coinConfig); + const builder = new ExportInPTxBuilder(this._coinConfig); // Initialize with the hex data (placeholder) builder.initBuilder({ txHex }); @@ -79,21 +39,56 @@ export class TransactionBuilderFactory { } /** - * Create a transaction builder for a specific type - * @param type - Transaction type + * Initialize Validator builder + * + * @returns {ValidatorTxBuilder} the builder initialized */ - getBuilder(type: TransactionType): AtomicTransactionBuilder { - switch (type) { - case TransactionType.Export: - return new ExportTxBuilder(this._coinConfig); - case TransactionType.Import: - return new ImportTxBuilder(this._coinConfig); - case TransactionType.AddValidator: - return new ValidatorTxBuilder(this._coinConfig); - case TransactionType.AddDelegator: - return new DelegatorTxBuilder(this._coinConfig); - default: - throw new NotImplementedError(`Transaction type ${type} not supported`); - } + getValidatorBuilder(): ValidatorTxBuilder { + return new ValidatorTxBuilder(this._coinConfig); + } + + /** + * Initialize Permissionless Validator builder + * + * @returns {PermissionlessValidatorTxBuilder} the builder initialized + */ + getPermissionlessValidatorTxBuilder(): PermissionlessValidatorTxBuilder { + return new PermissionlessValidatorTxBuilder(this._coinConfig); + } + + /** + * Export Cross chain transfer + * + * @returns {ExportInPTxBuilder} the builder initialized + */ + getExportBuilder(): ExportInPTxBuilder { + return new ExportInPTxBuilder(this._coinConfig); + } + + /** + * Import Cross chain transfer + * + * @returns {ImportInPTxBuilder} the builder initialized + */ + getImportBuilder(): ImportInPTxBuilder { + return new ImportInPTxBuilder(this._coinConfig); + } + + /** + * Import in C chain Cross chain transfer + * + * @returns {ImportInCTxBuilder} the builder initialized + */ + getImportInCBuilder(): ImportInCTxBuilder { + return new ImportInCTxBuilder(this._coinConfig); + } + + /** + * Export in C chain Cross chain transfer + * + * @returns {ExportInCTxBuilder} the builder initialized + */ + getExportInCBuilder(): ExportInCTxBuilder { + return new ExportInCTxBuilder(this._coinConfig); } } diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index b06b2608f0..04a454ce07 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -53,6 +53,28 @@ export class Utils implements BaseUtils { return `${prefix}-${bech32.encode(hrp, words)}`; }; + /** + * Convert a NodeID string to a Buffer. This is a Flare-specific implementation + * that doesn't rely on the Avalanche dependency. + * + * NodeIDs in Flare follow the format: NodeID- + * + * @param {string} nodeID - The NodeID string to convert + * @returns {Buffer} - The decoded NodeID as a Buffer + */ + // TODO add test cases for this method + public NodeIDStringToBuffer = (nodeID: string): Buffer => { + // Remove 'NodeID-' prefix if present + const cleanNodeID = nodeID.startsWith('NodeID-') ? nodeID.slice(7) : nodeID; + + try { + // Decode base58 string to buffer + return Buffer.from(bs58.decode(cleanNodeID)); + } catch (error) { + throw new Error(`Invalid NodeID format: ${error instanceof Error ? error.message : String(error)}`); + } + }; + public includeIn(walletAddresses: string[], otxoOutputAddresses: string[]): boolean { return walletAddresses.map((a) => otxoOutputAddresses.includes(a)).reduce((a, b) => a && b, true); } @@ -126,11 +148,23 @@ export class Utils implements BaseUtils { } } - public parseAddress = (address: string): Buffer => { - return this.stringToAddress(address); + // TODO add test cases for this method + public parseAddress = (address: string): string => { + return this.stringToAddress(address).toString(HEX_ENCODING); }; public stringToAddress = (address: string, hrp?: string): Buffer => { + // Handle hex addresses + if (address.startsWith('0x')) { + return Buffer.from(address.slice(2), 'hex'); + } + + // Handle raw hex without 0x prefix + if (/^[0-9a-fA-F]{40}$/.test(address)) { + return Buffer.from(address, 'hex'); + } + + // Handle Bech32 addresses const parts = address.trim().split('-'); if (parts.length < 2) { throw new Error('Error - Valid address should include -'); @@ -138,7 +172,7 @@ export class Utils implements BaseUtils { const split = parts[1].lastIndexOf('1'); if (split < 0) { - throw new Error('Error - Valid address must include separator (1)'); + throw new Error('Error - Valid bech32 address must include separator (1)'); } const humanReadablePart = parts[1].slice(0, split); diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts b/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts new file mode 100644 index 0000000000..f84fe9cd0d --- /dev/null +++ b/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts @@ -0,0 +1,24 @@ +// Test data for building export transactions with multiple P-addresses +export const EXPORT_IN_C = { + unsignedTxHex: + '0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000147c0b1f5d366ea8f1d0cd2ce108321d2be3b338600000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000009000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000008954270000000000000000000000020000000320829837bfba5d602b19cb9102b99eb3f895d5e47c71b9ae100e813e6332eddad2554ec12a0591fcbb6de9adcfbf2e0dfeffbe7792afd0c4085fdd370000000100000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000accf9fe1', + fullsigntxHex: + '0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000147c0b1f5d366ea8f1d0cd2ce108321d2be3b338600000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000009000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000008954270000000000000000000000020000000320829837bfba5d602b19cb9102b99eb3f895d5e47c71b9ae100e813e6332eddad2554ec12a0591fcbb6de9adcfbf2e0dfeffbe7792afd0c4085fdd37000000010000000900000001175782fa48b5b308d8f378832e5e637febb1fbcbec8f75fd6a65b657418575777b45c17a01b548a559d99e1a570d1efebe9b595dca383b0e39d5a961f16dd27500bdc89d2b', + txhash: 'jHRxuZjnSHYNwWpUUyob7RpfHwj1wfuQa8DGWQrkDh2RQ5Jb3', + + xPrivateKey: + 'xprv9s21ZrQH143K2DW9jvDoAkVpRKi5V9XhZaVdoUcqoYPPQ9wRrLNT6VGgWBbRoSYB39Lak6kXgdTM9T3QokEi5n2JJ8EdggHLkZPX8eDiBu1', + privateKey: 'a533d8419d4518e11cd8d9f049c73a8bdaf003d6602319f967ce3c243e646ba5', + amount: '8999975', + cHexAddress: '0x47c0b1f5d366ea8f1d0cd2ce108321d2be3b3386', + pAddresses: [ + 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + 'P-costwo1n4a86kc3td6nvmwm4xh0w78mc5jjxc9g8w6en0', + 'P-costwo1nhm2vw8653f3qwtj3kl6qa359kkt6y9r7qgljv', + ], + mainAddress: 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + targetChainId: '11111111111111111111111111111111LpoYY', + nonce: 9, + threshold: 2, + fee: '25', +}; diff --git a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts index 7d958a8b07..c5ea32d8e4 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts @@ -1,658 +1,102 @@ import { coins } from '@bitgo/statics'; import { BuildTransactionError } from '@bitgo/sdk-core'; import * as assert from 'assert'; -import { ExportInCTxBuilder } from '../../../src/lib/exportInCTxBuilder'; +import { TransactionBuilderFactory } from '../../../src/lib/transactionBuilderFactory'; +import { EXPORT_IN_C as testData } from '../../resources/transactionData/exportInC'; describe('ExportInCTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ExportInCTxBuilder; + const factory = new TransactionBuilderFactory(coins.get('tflrp')); + const txBuilder = factory.getExportInCBuilder(); - beforeEach(function () { - builder = new ExportInCTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ExportInCTxBuilder); - }); - - it('should extend AtomicInCTransactionBuilder', function () { - // Test inheritance - assert.ok(builder); - }); - }); - - describe('UTXO Override', function () { - it('should throw error when trying to set UTXOs', function () { - const mockUtxos = [{ id: 'test' }]; - - assert.throws( - () => { - builder.utxos(mockUtxos); - }, - BuildTransactionError, - 'Should reject UTXOs for C-chain export transactions' - ); - }); - - it('should throw error for empty UTXO array', function () { - assert.throws( - () => { - builder.utxos([]); - }, - BuildTransactionError, - 'Should reject empty UTXO array' - ); - }); - - it('should throw error for any UTXO input', function () { - const testCases = [[], [{}], ['invalid'], null, undefined]; - - testCases.forEach((testCase, index) => { - assert.throws( - () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - builder.utxos(testCase as any); - }, - BuildTransactionError, - `Test case ${index} should throw error` - ); - }); + describe('utxos', function () { + it('should throw an error when utxos are used', async function () { + assert.throws(() => { + txBuilder.utxos([]); + }, new BuildTransactionError('utxos are not required in Export Tx in C-Chain')); }); }); - describe('Amount Management', function () { - it('should set valid positive amounts', function () { - const validAmounts = ['1000', '1000000000000000000', '999999999999999999']; + describe('amount', function () { + it('should accept valid amounts in different formats', function () { + const validAmounts = [BigInt(1000), '1000', 1000]; validAmounts.forEach((amount) => { assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept amount: ${amount}`); - }); - }); - - it('should set bigint amounts', function () { - const bigintAmounts = [1000n, 1000000000000000000n, 999999999999999999n]; - - bigintAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept bigint amount: ${amount}`); - }); - }); - - it('should set numeric amounts', function () { - const numericAmounts = [1000, 2000000, 999999999]; - - numericAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept numeric amount: ${amount}`); + txBuilder.amount(amount); + }); }); }); - it('should reject zero amount', function () { - assert.throws(() => { - builder.amount(0); - }, /Amount must be positive/); - - assert.throws(() => { - builder.amount('0'); - }, /Amount must be positive/); - - assert.throws(() => { - builder.amount(0n); - }, /Amount must be positive/); - }); - - it('should reject negative amounts', function () { - const negativeAmounts = ['-1000', '-1']; + it('should throw error for invalid amounts', function () { + const invalidAmounts = ['0', '-1']; - negativeAmounts.forEach((amount) => { - assert.throws( - () => { - builder.amount(amount); - }, - BuildTransactionError, - `Should reject negative amount: ${amount}` - ); - }); - }); - - it('should handle large amounts', function () { - const largeAmounts = [ - '100000000000000000000000', // Very large amount - '18446744073709551615', // Near uint64 max - BigInt('999999999999999999999999999999'), - ]; - - largeAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should handle large amount: ${amount}`); - }); - }); - - it('should chain amount setting with other methods', function () { - const amount = '1000000000000000000'; - const nonce = 1n; - - assert.doesNotThrow(() => { - builder.amount(amount).nonce(nonce); + invalidAmounts.forEach((amount) => { + assert.throws(() => { + txBuilder.amount(amount); + }, BuildTransactionError); }); }); }); - describe('Nonce Management', function () { - it('should set valid nonce values', function () { - const validNonces = [0n, 1n, 1000n, 999999999999n]; + describe('nonce', function () { + it('should accept valid nonces in different formats', function () { + const validNonces = [BigInt(1), '1', 1, 0]; validNonces.forEach((nonce) => { assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept nonce: ${nonce}`); + txBuilder.nonce(nonce); + }); }); }); - it('should set string nonce values', function () { - const stringNonces = ['0', '1', '1000', '999999999999']; - - stringNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept string nonce: ${nonce}`); - }); - }); - - it('should set numeric nonce values', function () { - const numericNonces = [0, 1, 1000, 999999]; - - numericNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept numeric nonce: ${nonce}`); - }); - }); - - it('should reject negative nonce values', function () { - const negativeNonces = [-1n, -1000n]; - - negativeNonces.forEach((nonce) => { - assert.throws( - () => { - builder.nonce(nonce); - }, - BuildTransactionError, - `Should reject negative nonce: ${nonce}` - ); - }); - }); - - it('should reject negative string nonce values', function () { - const negativeStringNonces = ['-1', '-1000']; - - negativeStringNonces.forEach((nonce) => { - assert.throws( - () => { - builder.nonce(nonce); - }, - BuildTransactionError, - `Should reject negative string nonce: ${nonce}` - ); - }); - }); - - it('should handle large nonce values', function () { - const largeNonces = [ - '18446744073709551615', // Max uint64 - BigInt('999999999999999999999999999999'), - 1000000000000000000n, - ]; - - largeNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should handle large nonce: ${nonce}`); - }); - }); - - it('should chain nonce setting with other methods', function () { - const nonce = 123n; - const amount = '1000000000000000000'; - - assert.doesNotThrow(() => { - builder.nonce(nonce).amount(amount); - }); - }); - }); - - describe('Destination Address Management', function () { - it('should set single destination address', function () { - const singleAddress = 'P-flare1destination'; - - assert.doesNotThrow(() => { - builder.to(singleAddress); - }); - }); - - it('should set multiple destination addresses', function () { - const multipleAddresses = ['P-flare1dest1', 'P-flare1dest2', 'P-flare1dest3']; - - assert.doesNotThrow(() => { - builder.to(multipleAddresses); - }); - }); - - it('should handle comma-separated addresses', function () { - const commaSeparated = 'P-flare1dest1~P-flare1dest2~P-flare1dest3'; - - assert.doesNotThrow(() => { - builder.to(commaSeparated); - }); - }); - - it('should handle empty address array', function () { - assert.doesNotThrow(() => { - builder.to([]); - }); - }); - - it('should chain address setting with other methods', function () { - const addresses = ['P-flare1dest1', 'P-flare1dest2']; - const amount = '1000000000000000000'; - - assert.doesNotThrow(() => { - builder.to(addresses).amount(amount); - }); - }); - }); - - describe('Transaction Type Verification', function () { - it('should verify transaction type (placeholder returns true)', function () { - const mockTx = { type: 'export' }; - const result = ExportInCTxBuilder.verifyTxType(mockTx); - assert.strictEqual(result, true); // Placeholder always returns true - }); - - it('should handle different transaction objects', function () { - const testCases = [{}, null, undefined, { type: 'import' }, { data: 'test' }]; - - testCases.forEach((testCase, index) => { - const result = ExportInCTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, true, `Test case ${index} should return true (placeholder)`); - }); - }); - - it('should verify via instance method', function () { - const mockTx = { type: 'export' }; - const result = builder.verifyTxType(mockTx); - assert.strictEqual(result, true); - }); - }); - - describe('Transaction Building', function () { - it('should handle building when transaction has credentials', function () { - const mockTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - builder.initBuilder(mockTx); - - // Should not throw when credentials exist - assert.doesNotThrow(() => { - // Access protected method via type assertion - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }); - }); - - it('should require amount for building', function () { - builder.nonce(1n); - builder.to(['P-flare1dest']); - - // Mock setting from addresses via transaction initialization - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - builder.initBuilder(mockRawTx); - // Clear amount to test error - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any)._amount = undefined; - + it('should throw error for negative nonce', function () { assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }, Error); + txBuilder.nonce('-1'); + }, new BuildTransactionError('Nonce must be greater or equal than 0')); }); }); - describe('Transaction Initialization', function () { - it('should initialize from raw transaction object', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - + describe('to', function () { + const txBuilder = factory.getExportInCBuilder(); + it('should accept multiple P-addresses', function () { + const pAddresses = testData.pAddresses; assert.doesNotThrow(() => { - builder.initBuilder(mockRawTx); + txBuilder.to(pAddresses); }); }); - - it('should validate blockchain ID during initialization', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.from('wrong-blockchain'), - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, Error); - }); - - it('should validate single output requirement', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default // Will match default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest1'], - amount: 500000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - { - addresses: ['P-flare1dest2'], - amount: 500000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); - }); - - it('should validate single input requirement', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test1', - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - { - address: 'C-flare1test2', - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 2n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); - }); - - it('should validate negative fee calculation', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 500000000000000000n, // Less than output - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, // More than input - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); - }); - }); - - describe('From Implementation', function () { - it('should handle string raw transaction', function () { - const rawString = 'hex-encoded-transaction'; - + it('should accept single P-address', function () { assert.doesNotThrow(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).fromImplementation(rawString); + txBuilder.to(testData.pAddresses[0]); }); }); - it('should handle object raw transaction', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; + it('should accept tilde-separated P-addresses string', function () { + const pAddresses = testData.pAddresses.join('~'); assert.doesNotThrow(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).fromImplementation(mockRawTx); + txBuilder.to(pAddresses); }); }); }); - describe('Integration Tests', function () { - it('should handle complete export flow preparation', function () { - const amount = '1000000000000000000'; // 1 FLR - const nonce = 123n; - const toAddresses = ['P-flare1destination']; - - // Complete setup - builder.amount(amount).nonce(nonce).to(toAddresses); - - // All operations should complete without throwing - assert.ok(true); - }); - - it('should handle method chaining extensively', function () { - // Test extensive method chaining - assert.doesNotThrow(() => { - builder - .amount('5000000000000000000') // 5 FLR - .nonce(456n) - .to(['P-flare1receiver1', 'P-flare1receiver2']); - }); - }); - - it('should handle large transaction values', function () { - const largeAmount = '100000000000000000000000'; // 100k FLR - const largeNonce = 999999999999n; - - assert.doesNotThrow(() => { - builder.amount(largeAmount).nonce(largeNonce); - }); - }); - - it('should handle multiple destination addresses', function () { - const multipleDestinations = [ - 'P-flare1dest1', - 'P-flare1dest2', - 'P-flare1dest3', - 'P-flare1dest4', - 'P-flare1dest5', - ]; - - assert.doesNotThrow(() => { - builder.amount('1000000000000000000').to(multipleDestinations); - }); - }); - }); - - describe('Edge Cases', function () { - it('should handle zero values appropriately', function () { - // Zero amount should be rejected - assert.throws(() => { - builder.amount(0); - }, /Amount must be positive/); - - // Zero nonce should be valid - assert.doesNotThrow(() => { - builder.nonce(0n); - }); - }); - - it('should handle maximum values', function () { - const maxBigInt = BigInt('18446744073709551615'); // Max uint64 - - assert.doesNotThrow(() => { - builder.amount(maxBigInt); - }); - - assert.doesNotThrow(() => { - builder.nonce(maxBigInt); - }); - }); - - it('should maintain state across multiple operations', function () { - // Build state incrementally - builder.amount('1000000000000000000'); - builder.nonce(123n); - builder.to(['P-flare1dest']); - - // State should be maintained across operations - assert.ok(true); + describe('should build a export txn from C to P', () => { + const newTxBuilder = () => + factory + .getExportInCBuilder() + .fromPubKey(testData.cHexAddress) + .nonce(testData.nonce) + .amount(testData.amount) + .threshold(testData.threshold) + .locktime(10) + .to(testData.pAddresses) + .feeRate(testData.fee); + + it('Should create export tx for same values', async () => { + const txBuilder = newTxBuilder(); + + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.unsignedTxHex); }); }); }); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 2bf1c36493..58c331d5d9 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -9,8 +9,8 @@ export interface FlareNetwork extends BaseNetwork { batcherContractAddress?: string; forwarderFactoryAddress?: string; forwarderImplementationAddress?: string; - blockchainID?: string; - cChainBlockchainID?: string; + blockchainID: string; + cChainBlockchainID: string; networkID?: number; hrp?: string; alias?: string; @@ -1835,7 +1835,7 @@ export class FlarePTestnet extends Testnet implements FlareNetwork { minDelegationFee = '0'; } -export class Flare extends Mainnet implements FlareNetwork, EthereumNetwork { +export class Flare extends Mainnet implements EthereumNetwork { name = 'Flarechain'; family = CoinFamily.FLR; explorerUrl = 'https://flare-explorer.flare.network/tx/'; @@ -1849,7 +1849,7 @@ export class Flare extends Mainnet implements FlareNetwork, EthereumNetwork { forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; } -export class FlareTestnet extends Testnet implements FlareNetwork, EthereumNetwork { +export class FlareTestnet extends Testnet implements EthereumNetwork { name = 'FlarechainTestnet'; family = CoinFamily.FLR; explorerUrl = 'https://coston2-explorer.flare.network/tx/';