diff --git a/modules/sdk-coin-flrp/package.json b/modules/sdk-coin-flrp/package.json index fb4df0e151..45161fc2ca 100644 --- a/modules/sdk-coin-flrp/package.json +++ b/modules/sdk-coin-flrp/package.json @@ -42,18 +42,16 @@ ".ts" ] }, - "devDependencies": { - "@bitgo/sdk-api": "^1.71.7", - "@bitgo/sdk-test": "^9.1.15" - }, "dependencies": { "@bitgo/sdk-core": "^36.22.0", - "@bitgo/secp256k1": "^1.7.0", "@bitgo/statics": "^58.15.0", - "@flarenetwork/flarejs": "4.1.0-rc0", + "@bitgo/secp256k1": "^1.7.0", + "@flarenetwork/flarejs": "4.1.1", "bech32": "^2.0.0", "bignumber.js": "9.0.0", - "bs58": "^6.0.0" + "bs58": "^6.0.0", + "create-hash": "^1.2.0", + "safe-buffer": "^5.2.1" }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c", "files": [ diff --git a/modules/sdk-coin-flrp/src/flrp.ts b/modules/sdk-coin-flrp/src/flrp.ts index f42b7e434b..408254ad96 100644 --- a/modules/sdk-coin-flrp/src/flrp.ts +++ b/modules/sdk-coin-flrp/src/flrp.ts @@ -1,34 +1,19 @@ -import { FlareNetwork, BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics'; +import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics'; import { + AuditDecryptedKeyParams, BaseCoin, BitGoBase, KeyPair, - VerifyAddressOptions, - SignedTransaction, - ParseTransactionOptions, - BaseTransaction, - InvalidTransactionError, - SigningError, - TransactionType, - InvalidAddressError, - UnexpectedAddressError, - ITransactionRecipient, - ParsedTransaction, MultisigType, multisigTypes, - AuditDecryptedKeyParams, - MethodNotImplementedError, + ParsedTransaction, + ParseTransactionOptions, + SignedTransaction, + SignTransactionOptions, + TssVerifyAddressOptions, + VerifyAddressOptions, + VerifyTransactionOptions, } from '@bitgo/sdk-core'; -import * as FlrpLib from './lib'; -import { - FlrpSignTransactionOptions, - ExplainTransactionOptions, - FlrpVerifyTransactionOptions, - FlrpTransactionStakingOptions, - FlrpTransactionParams, -} from './lib/iface'; -import utils from './lib/utils'; -import BigNumber from 'bignumber.js'; export class Flrp extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -65,308 +50,28 @@ export class Flrp extends BaseCoin { return multisigTypes.onchain; } - /** - * Check if staking txn is valid, based on expected tx params. - * - * @param {FlrpTransactionStakingOptions} stakingOptions expected staking params to check against - * @param {FlrpLib.TransactionExplanation} explainedTx explained staking transaction - */ - validateStakingTx(stakingOptions: FlrpTransactionStakingOptions, explainedTx: FlrpLib.TransactionExplanation): void { - const filteredRecipients = [{ address: stakingOptions.nodeID, amount: stakingOptions.amount }]; - const filteredOutputs = explainedTx.outputs.map((output) => utils.pick(output, ['address', 'amount'])); - - if (!utils.isEqual(filteredOutputs, filteredRecipients)) { - throw new Error('Tx outputs does not match with expected txParams'); - } - if (stakingOptions?.amount !== explainedTx.outputAmount) { - throw new Error('Tx total amount does not match with expected total amount field'); - } - } - - /** - * Check if export txn is valid, based on expected tx params. - * - * @param {ITransactionRecipient[]} recipients expected recipients and info - * @param {FlrpLib.TransactionExplanation} explainedTx explained export transaction - */ - validateExportTx(recipients: ITransactionRecipient[], explainedTx: FlrpLib.TransactionExplanation): void { - if (recipients.length !== 1 || explainedTx.outputs.length !== 1) { - throw new Error('Export Tx requires one recipient'); - } - - const maxImportFee = (this._staticsCoin.network as FlareNetwork).maxImportFee || '0'; - const recipientAmount = new BigNumber(recipients[0].amount); - if ( - recipientAmount.isGreaterThan(explainedTx.outputAmount) || - recipientAmount.plus(maxImportFee).isLessThan(explainedTx.outputAmount) - ) { - throw new Error( - `Tx total amount ${explainedTx.outputAmount} does not match with expected total amount field ${recipientAmount} and max import fee ${maxImportFee}` - ); - } - - if (explainedTx.outputs && !utils.isValidAddress(explainedTx.outputs[0].address)) { - throw new Error(`Invalid P-chain address ${explainedTx.outputs[0].address}`); - } + verifyTransaction(params: VerifyTransactionOptions): Promise { + throw new Error('Method not implemented.'); } - - /** - * Check if import txn into P is valid, based on expected tx params. - * - * @param {FlrpLib.FlrpEntry[]} explainedTxInputs tx inputs (unspents to be imported) - * @param {FlrpTransactionParams} txParams expected tx info to check against - */ - validateImportTx(explainedTxInputs: FlrpLib.FlrpEntry[], txParams: FlrpTransactionParams): void { - if (txParams.unspents) { - if (explainedTxInputs.length !== txParams.unspents.length) { - throw new Error(`Expected ${txParams.unspents.length} UTXOs, transaction had ${explainedTxInputs.length}`); - } - - const unspents = new Set(txParams.unspents); - - for (const unspent of explainedTxInputs) { - if (!unspents.has(unspent.id)) { - throw new Error(`Transaction should not contain the UTXO: ${unspent.id}`); - } - } - } + isWalletAddress(params: VerifyAddressOptions | TssVerifyAddressOptions): Promise { + throw new Error('Method not implemented.'); } - - async verifyTransaction(params: FlrpVerifyTransactionOptions): Promise { - const txHex = params.txPrebuild && params.txPrebuild.txHex; - if (!txHex) { - throw new Error('missing required tx prebuild property txHex'); - } - let tx; - try { - const txBuilder = this.getBuilder().from(txHex); - tx = await txBuilder.build(); - } catch (error) { - throw new Error('Invalid transaction'); - } - const explainedTx = tx.explainTransaction(); - - const { type, stakingOptions } = params.txParams; - // TODO(BG-62112): change ImportToC type to Import - if (!type || (type !== 'ImportToC' && explainedTx.type !== TransactionType[type])) { - throw new Error('Tx type does not match with expected txParams type'); - } - - switch (explainedTx.type) { - // @deprecated - case TransactionType.AddDelegator: - case TransactionType.AddValidator: - case TransactionType.AddPermissionlessDelegator: - case TransactionType.AddPermissionlessValidator: - if (stakingOptions) { - this.validateStakingTx(stakingOptions, explainedTx); - } - break; - case TransactionType.Export: - if (!params.txParams.recipients || params.txParams.recipients?.length !== 1) { - throw new Error('Export Tx requires a recipient'); - } else { - this.validateExportTx(params.txParams.recipients, explainedTx); - } - break; - case TransactionType.Import: - if (tx.isTransactionForCChain) { - // Import to C-chain - if (explainedTx.outputs.length !== 1) { - throw new Error('Expected 1 output in import transaction'); - } - if (!params.txParams.recipients || params.txParams.recipients.length !== 1) { - throw new Error('Expected 1 recipient in import transaction'); - } - } else { - // Import to P-chain - if (explainedTx.outputs.length !== 1) { - throw new Error('Expected 1 output in import transaction'); - } - this.validateImportTx(explainedTx.inputs, params.txParams); - } - break; - default: - throw new Error('Tx type is not supported yet'); - } - return true; + parseTransaction(params: ParseTransactionOptions): Promise { + throw new Error('Method not implemented.'); } - - /** - * Check if address is valid, then make sure it matches the root address. - * - * @param params.address address to validate - * @param params.keychains public keys to generate the wallet - */ - async isWalletAddress(params: VerifyAddressOptions): Promise { - const { address, keychains } = params; - - if (!this.isValidAddress(address)) { - throw new InvalidAddressError(`invalid address: ${address}`); - } - if (!keychains || keychains.length !== 3) { - throw new Error('Invalid keychains'); - } - - // multisig addresses are separated by ~ - const splitAddresses = address.split('~'); - - // derive addresses from keychain - const unlockAddresses = keychains.map((keychain) => - new FlrpLib.KeyPair({ pub: keychain.pub }).getAddress(this._staticsCoin.network.type) - ); - - if (splitAddresses.length !== unlockAddresses.length) { - throw new UnexpectedAddressError(`address validation failure: multisig address length does not match`); - } - - if (!this.adressesArraysMatch(splitAddresses, unlockAddresses)) { - throw new UnexpectedAddressError(`address validation failure: ${address} is not of this wallet`); - } - - return true; - } - - /** - * Validate that two multisig address arrays have the same elements, order doesnt matter - * @param addressArray1 - * @param addressArray2 - * @returns true if address arrays have the same addresses - * @private - */ - private adressesArraysMatch(addressArray1: string[], addressArray2: string[]) { - return JSON.stringify(addressArray1.sort()) === JSON.stringify(addressArray2.sort()); - } - - /** - * Generate Flrp key pair - * - * @param {Buffer} seed - Seed from which the new keypair should be generated, otherwise a random seed is used - * @returns {Object} object with generated pub and prv - */ generateKeyPair(seed?: Buffer): KeyPair { - const keyPair = seed ? new FlrpLib.KeyPair({ seed }) : new FlrpLib.KeyPair(); - const keys = keyPair.getKeys(); - - if (!keys.prv) { - throw new Error('Missing prv in key generation.'); - } - - return { - pub: keys.pub, - prv: keys.prv, - }; + throw new Error('Method not implemented.'); } - - /** - * Return boolean indicating whether input is valid public key for the coin - * - * @param {string} pub the prv to be checked - * @returns is it valid? - */ isValidPub(pub: string): boolean { - try { - new FlrpLib.KeyPair({ pub }); - return true; - } catch (e) { - return false; - } + throw new Error('Method not implemented.'); } - - /** - * Return boolean indicating whether input is valid private key for the coin - * - * @param {string} prv the prv to be checked - * @returns is it valid? - */ - isValidPrv(prv: string): boolean { - try { - new FlrpLib.KeyPair({ prv }); - return true; - } catch (e) { - return false; - } + isValidAddress(address: string): boolean { + throw new Error('Method not implemented.'); } - - isValidAddress(address: string | string[]): boolean { - if (address === undefined) { - return false; - } - - // validate eth address for cross-chain txs to c-chain - if (typeof address === 'string' && utils.isValidEthereumAddress(address)) { - return true; - } - - return FlrpLib.Utils.isValidAddress(address); + signTransaction(params: SignTransactionOptions): Promise { + throw new Error('Method not implemented.'); } - - /** - * Signs Flrp transaction - */ - async signTransaction(params: FlrpSignTransactionOptions): Promise { - // deserialize raw transaction (note: fromAddress has onchain order) - const txBuilder = this.getBuilder().from(params.txPrebuild.txHex); - const key = params.prv; - - // push the keypair to signer array - txBuilder.sign({ key }); - - // build the transaction - const transaction: BaseTransaction = await txBuilder.build(); - if (!transaction) { - throw new InvalidTransactionError('Error while trying to build transaction'); - } - return transaction.signature.length >= 2 - ? { txHex: transaction.toBroadcastFormat() } - : { halfSigned: { txHex: transaction.toBroadcastFormat() } }; - } - - async parseTransaction(params: ParseTransactionOptions): Promise { - return {}; - } - - /** - * Explain a Flrp transaction from txHex - * @param params - * @param callback - */ - async explainTransaction(params: ExplainTransactionOptions): Promise { - const txHex = params.txHex ?? params?.halfSigned?.txHex; - if (!txHex) { - throw new Error('missing transaction hex'); - } - try { - const txBuilder = this.getBuilder().from(txHex); - const tx = await txBuilder.build(); - return tx.explainTransaction(); - } catch (e) { - throw new Error(`Invalid transaction: ${e.message}`); - } - } - - recoverySignature(message: Buffer, signature: Buffer): Buffer { - return FlrpLib.Utils.recoverySignature(this._staticsCoin.network as FlareNetwork, message, signature); - } - - async signMessage(key: KeyPair, message: string | Buffer): Promise { - const prv = new FlrpLib.KeyPair(key).getPrivateKey(); - if (!prv) { - throw new SigningError('Invalid key pair options'); - } - if (typeof message === 'string') { - message = Buffer.from(message, 'hex'); - } - return FlrpLib.Utils.createSignature(this._staticsCoin.network as FlareNetwork, message, prv); - } - - private getBuilder(): FlrpLib.TransactionBuilderFactory { - return new FlrpLib.TransactionBuilderFactory(coins.get(this.getChain())); - } - - /** @inheritDoc */ auditDecryptedKey(params: AuditDecryptedKeyParams): void { - /** https://bitgoinc.atlassian.net/browse/COIN-4213 */ - throw new MethodNotImplementedError(); + throw new Error('Method not implemented.'); } } diff --git a/modules/sdk-coin-flrp/src/iface.ts b/modules/sdk-coin-flrp/src/iface.ts deleted file mode 100644 index 0adf50ec9e..0000000000 --- a/modules/sdk-coin-flrp/src/iface.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - SignTransactionOptions, - TransactionPrebuild as BaseTransactionPrebuild, - TransactionRecipient, - TransactionFee, -} from '@bitgo/sdk-core'; - -export { TransactionFee }; -export interface ExplainTransactionOptions { - txHex?: string; - halfSigned?: { - txHex: string; - }; - publicKeys?: string[]; -} - -export interface TxInfo { - recipients: TransactionRecipient[]; - from: string; - txid: string; -} - -export interface FlrpSignTransactionOptions extends SignTransactionOptions { - txPrebuild: TransactionPrebuild; - prv: string | string[]; - pubKeys?: string[]; -} -export interface TransactionPrebuild extends BaseTransactionPrebuild { - txHex: string; - txInfo: TxInfo; - source: string; -} diff --git a/modules/sdk-coin-flrp/src/index.ts b/modules/sdk-coin-flrp/src/index.ts index 7c77032559..2fbf3c10c1 100644 --- a/modules/sdk-coin-flrp/src/index.ts +++ b/modules/sdk-coin-flrp/src/index.ts @@ -1,5 +1,4 @@ export * from './flrp'; export * from './tflrp'; export * as FlrPLib from './lib'; -export * from './iface'; export * from './register'; diff --git a/modules/sdk-coin-flrp/src/lib/ExportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ExportInCTxBuilder.ts new file mode 100644 index 0000000000..3b2bf5dfbb --- /dev/null +++ b/modules/sdk-coin-flrp/src/lib/ExportInCTxBuilder.ts @@ -0,0 +1,206 @@ +import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; +import { + evmSerial, + UnsignedTx, + Credential, + BigIntPr, + Int, + Id, + TransferableOutput, + Address, + TransferOutput, + OutputOwners, + utils as FlareUtils, +} from '@flarenetwork/flarejs'; +import utils from './utils'; +import { DecodedUtxoObj, Tx, FlareTransactionType } from './iface'; + +export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { + private _amount: bigint; + private _nonce: bigint; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** + * Utxos are not required in Export Tx in C-Chain. + * Override utxos to prevent used by throwing a error. + * + * @param {DecodedUtxoObj[]} value ignored + */ + utxos(value: DecodedUtxoObj[]): this { + throw new BuildTransactionError('utxos are not required in Export Tx in C-Chain'); + } + + /** + * Amount is a bigint that specifies the quantity of the asset that this output owns. Must be positive. + * The transaction output amount add a fixed fee that will be paid upon import. + * + * @param {bigint | string} amount The withdrawal amount + */ + amount(amount: bigint | string): this { + const amountBigInt = typeof amount === 'string' ? BigInt(amount) : amount; + this.validateAmount(amountBigInt); + this._amount = amountBigInt; + return this; + } + + /** + * Set the nonce of C-Chain sender address + * + * @param {number | string} nonce - number that can be only used once + */ + nonce(nonce: number | string): this { + const nonceBigInt = BigInt(nonce); + this.validateNonce(nonceBigInt); + this._nonce = nonceBigInt; + return this; + } + + /** + * Export tx target P wallet. + * + * @param pAddresses + */ + to(pAddresses: string | string[]): this { + const pubKeys = Array.isArray(pAddresses) ? pAddresses : pAddresses.split('~'); + this.transaction._to = pubKeys.map((addr) => utils.parseAddress(addr)); + return this; + } + + protected get transactionType(): TransactionType { + return TransactionType.Export; + } + + initBuilder(tx: Tx): this { + const baseTx = tx as evmSerial.ExportTx; + if (!this.verifyTxType(baseTx._type)) { + throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type'); + } + + // The outputs is a multisign P-Chain address result. + // It's expected to have only one output to the destination P-Chain address. + const outputs = baseTx.exportedOutputs; + if (outputs.length !== 1) { + throw new BuildTransactionError('Transaction can have one output'); + } + const output = outputs[0]; + + // TODO validate assetId + + // The inputs is not an utxo. + // It's expected to have only one input from C-Chain address. + const inputs = baseTx.ins; + if (inputs.length !== 1) { + throw new BuildTransactionError('Transaction can have one input'); + } + const input = inputs[0]; + + const transferOutput = output.output as TransferOutput; + const owners = transferOutput.getOwners(); + this.transaction._to = owners; + const inputAmount = input.amount.value(); + const outputAmount = transferOutput.amount(); + const fee = inputAmount - outputAmount; + this._amount = outputAmount; + 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())]; + this.transaction._locktime = transferOutput.getLocktime(); + + this._nonce = input.nonce.value(); + this.transaction.setTransaction(tx); + return this; + } + + static verifyTxType(txnType: string): boolean { + return txnType === FlareTransactionType.EvmExportTx; + } + + verifyTxType(txnType: string): boolean { + return ExportInCTxBuilder.verifyTxType(txnType); + } + + /** + * Build the export in C-chain transaction + * @protected + */ + protected buildFlareTransaction(): void { + if (this.transaction.hasCredentials) return; + if (this._amount === undefined) { + throw new Error('amount is required'); + } + if (this.transaction._fromAddresses.length !== 1) { + throw new Error('sender is one and required'); + } + if (this.transaction._to.length === 0) { + throw new Error('to is required'); + } + if (!this.transaction._fee.feeRate) { + throw new Error('fee rate is required'); + } + if (this._nonce === undefined) { + throw new Error('nonce is required'); + } + + const txFee = BigInt(this.fixedFee); + const fee = BigInt(this.transaction._fee.feeRate) + txFee; + this.transaction._fee.fee = fee.toString(); + this.transaction._fee.size = 1; + + const fromAddressBytes = this.transaction._fromAddresses[0]; + const fromAddress = new Address(fromAddressBytes); + const assetId = utils.flareIdString(this.transaction._assetId); + const amount = new BigIntPr(this._amount + fee); + const nonce = new BigIntPr(this._nonce); + const input = new evmSerial.Input(fromAddress, amount, assetId, nonce); + const exportTx = new evmSerial.ExportTx( + new Int(this.transaction._networkID), + utils.flareIdString(this.transaction._blockchainID), + new Id(new Uint8Array(this._externalChainId)), + [input], + [ + new TransferableOutput( + assetId, + new TransferOutput( + new BigIntPr(this._amount), + new OutputOwners(new BigIntPr(this.transaction._locktime), new Int(this.transaction._threshold), [ + new Address(this.transaction._to[0]), + ]) + ) + ), + ] + ); + + // Create address maps with proper EVM address format + const addressMap = new FlareUtils.AddressMap([ + [fromAddress, 0], + [fromAddress, 1], // Map the same address to both indices since it's used in both places + ]); + const addressMaps = new FlareUtils.AddressMaps([addressMap]); // Single map is sufficient + + // Create unsigned transaction with proper address mapping + const unsignedTx = new UnsignedTx( + exportTx, + [], // Empty UTXOs array, will be filled during processing + addressMaps, + [new Credential([utils.createNewSig('')])] // Empty credential for signing + ); + + this.transaction.setTransaction(unsignedTx); + } + + /** + * Check the nonce is non-negative. + * @param nonce + */ + validateNonce(nonce: bigint): void { + if (nonce < BigInt(0)) { + throw new BuildTransactionError('Nonce must be greater or equal than 0'); + } + } +} diff --git a/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts new file mode 100644 index 0000000000..eb3cc13591 --- /dev/null +++ b/modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts @@ -0,0 +1,178 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core'; +import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; +import { + evmSerial, + UnsignedTx, + BigIntPr, + Int, + Id, + TransferableOutput, + Address, + utils as FlareUtils, + TransferOutput, + OutputOwners, +} from '@flarenetwork/flarejs'; +import utils from './utils'; +import { DecodedUtxoObj, SECP256K1_Transfer_Output, FlareTransactionType, Tx } from './iface'; + +export class ExportInPTxBuilder extends AtomicTransactionBuilder { + private _amount: bigint; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._externalChainId = utils.cb58Decode(this.transaction._network.cChainBlockchainID); + } + + protected get transactionType(): TransactionType { + return TransactionType.Export; + } + + /** + * Amount is a bigint that specifies the quantity of the asset that this output owns. Must be positive. + * @param {bigint | string} amount The withdrawal amount + */ + amount(value: bigint | string): this { + const valueBigInt = typeof value === 'string' ? BigInt(value) : value; + this.validateAmount(valueBigInt); + this._amount = valueBigInt; + return this; + } + + initBuilder(tx: Tx): this { + const baseTx = tx as evmSerial.ExportTx; + if (!this.verifyTxType(baseTx._type)) { + throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type'); + } + + // The exportedOutputs is a TransferableOutput array. + // It's expected to have only one output with the addresses of the sender. + const outputs = baseTx.exportedOutputs; + if (outputs.length !== 1) { + throw new BuildTransactionError('Transaction can have one external output'); + } + + const output = outputs[0]; + const outputTransfer = output.output as TransferOutput; + const assetId = output.assetId.toBytes(); + if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId)) !== 0) { + throw new Error('The Asset ID of the output does not match the transaction'); + } + + // Set locktime to 0 since it's not used in EVM outputs + this.transaction._locktime = BigInt(0); + + // Set threshold to 1 since EVM outputs only have one address + this.transaction._threshold = 1; + + // Convert output address to buffer and set as fromAddress + const outputOwners = outputTransfer as unknown as OutputOwners; + this.transaction._fromAddresses = outputOwners.addrs.map((addr) => Buffer.from(addr.toBytes())); + + // Set external chain ID from the destination chain + this._externalChainId = Buffer.from(baseTx.destinationChain.toString()); + + // Set amount from output + this._amount = outputTransfer.amount(); + + // Recover UTXOs from inputs + this.transaction._utxos = this.recoverUtxos(baseTx.ins); + return this; + } + + static verifyTxType(txnType: string): boolean { + return txnType === FlareTransactionType.PvmExportTx; + } + + verifyTxType(txnType: string): boolean { + return ExportInPTxBuilder.verifyTxType(txnType); + } + + /** + * Build the export transaction + * @protected + */ + protected buildFlareTransaction(): void { + // if tx has credentials, tx shouldn't change + if (this.transaction.hasCredentials) return; + + const { inputs, credentials } = this.createInputOutput(this._amount + BigInt(this.transaction.fee.fee)); + + // Convert TransferableInputs to EVM Inputs + const transferableInputs: evmSerial.Input[] = inputs.map((input) => { + const assetIdBytes = input.assetId.toBytes(); + const inputOwners = input as unknown as OutputOwners; + return new evmSerial.Input( + inputOwners.addrs[0], + new BigIntPr(input.amount()), + new Id(assetIdBytes), + new BigIntPr(BigInt(0)) // nonce is 0 for non-EVM inputs + ); + }); + + // Create the export transaction + const exportTx = new evmSerial.ExportTx( + new Int(this.transaction._networkID), + new Id(new Uint8Array(Buffer.from(this.transaction._blockchainID, 'hex'))), + new Id(new Uint8Array(this._externalChainId)), + transferableInputs, + this.exportedOutputs() + ); + + // Create unsigned transaction with proper address maps + const addressMap = new FlareUtils.AddressMap([[new Address(this.transaction._fromAddresses[0]), 0]]); + const addressMaps = new FlareUtils.AddressMaps([addressMap]); + + const unsignedTx = new UnsignedTx( + exportTx, + [], // Empty UTXOs array, will be filled during processing + addressMaps, + credentials + ); + + this.transaction.setTransaction(unsignedTx); + } + + /** + * Create the ExportedOutputs where the recipient address are the sender. + * Later an importTx should complete the operations signing with the same keys. + * @protected + */ + protected exportedOutputs(): TransferableOutput[] { + const assetIdBytes = new Uint8Array(Buffer.from(this.transaction._assetId, 'hex')); + const outputOwners = new OutputOwners( + new BigIntPr(BigInt(0)), // locktime + new Int(1), // threshold + [new Address(this.transaction._fromAddresses[0])] + ); + + const output = new TransferOutput(new BigIntPr(this._amount), outputOwners); + + return [new TransferableOutput(new Id(assetIdBytes), output)]; + } + + /** + * Recover UTXOs from inputs + * @param inputs Array of inputs + * @returns Array of decoded UTXO objects + */ + private recoverUtxos(inputs: evmSerial.Input[]): DecodedUtxoObj[] { + return inputs.map((input) => { + const txid = Buffer.from(input.assetId.toBytes()).toString('hex'); + return { + outputID: SECP256K1_Transfer_Output, + amount: input.amount.toString(), + txid: utils.cb58Encode(Buffer.from(txid, 'hex')), + outputidx: '0', // Since EVM inputs don't have output indices + threshold: this.transaction._threshold, + addresses: [ + utils.addressToString( + this.transaction._network.hrp, + this.transaction._network.alias, + Buffer.from(input.address.toBytes()) + ), + ], + }; + }); + } +} diff --git a/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts new file mode 100644 index 0000000000..a73596be95 --- /dev/null +++ b/modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts @@ -0,0 +1,271 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core'; +import { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; +import { + evmSerial, + UnsignedTx, + Credential, + BigIntPr, + Int, + Id, + TransferableInput, + TypeSymbols, + Address, + utils as FlareUtils, + avmSerial, +} from '@flarenetwork/flarejs'; +import utils from './utils'; +import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface'; + +export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** + * C-chain address who is target of the import. + * Address format is eth like + * @param {string} cAddress + */ + to(cAddress: string): this { + this.transaction._to = [utils.parseAddress(cAddress)]; + return this; + } + + protected get transactionType(): TransactionType { + return TransactionType.Import; + } + + initBuilder(tx: Tx): this { + const baseTx = tx as evmSerial.ImportTx; + if (!this.verifyTxType(baseTx._type)) { + throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type'); + } + + // The outputs is a single C-Chain address result. + // It's expected to have only one output to the destination C-Chain address. + const outputs = baseTx.Outs; + if (outputs.length !== 1) { + throw new BuildTransactionError('Transaction can have one output'); + } + const output = outputs[0]; + + const assetIdStr = Buffer.from(this.transaction._assetId).toString('hex'); + if (Buffer.from(output.assetId.toBytes()).toString('hex') !== assetIdStr) { + throw new Error('AssetID are not equals'); + } + this.transaction._to = [Buffer.from(output.address.toBytes())]; + + const inputs = baseTx.importedInputs; + this.transaction._utxos = this.recoverUtxos(inputs); + + // Calculate total input and output amounts + const totalInputAmount = inputs.reduce((t, i) => t + i.amount(), BigInt(0)); + const totalOutputAmount = output.amount.value(); + + // Calculate fee based on input/output difference + const fee = totalInputAmount - totalOutputAmount; + const feeSize = this.calculateFeeSize(baseTx); + const feeRate = Number(fee) / feeSize; + + this.transaction._fee = { + fee: fee.toString(), + feeRate: feeRate, + size: feeSize, + }; + + this.transaction.setTransaction(tx); + return this; + } + + static verifyTxType(txnType: string): boolean { + return txnType === FlareTransactionType.PvmImportTx; + } + + verifyTxType(txnType: string): boolean { + return ImportInCTxBuilder.verifyTxType(txnType); + } + + /** + * Build the import in C-chain transaction + * @protected + */ + protected buildFlareTransaction(): void { + // if tx has credentials, tx shouldn't change + if (this.transaction.hasCredentials) return; + if (this.transaction._to.length !== 1) { + throw new Error('to is required'); + } + if (!this.transaction._fee.feeRate) { + throw new Error('fee rate is required'); + } + + const { inputs, amount, credentials } = this.createInputs(); + + // Calculate fee + 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 output with required interface implementation + const output = { + _type: TypeSymbols.BaseTx, + address: new Address(this.transaction._to[0]), + amount: new BigIntPr(amount - fee), + assetId: new Id(new Uint8Array(Buffer.from(this.transaction._assetId, 'hex'))), + toBytes: () => new Uint8Array(), + }; + + // Create the import transaction + const importTx = 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, + [output] + ); + + // Create unsigned transaction + const addressMap = new FlareUtils.AddressMap([[new Address(this.transaction._fromAddresses[0]), 0]]); + const addressMaps = new FlareUtils.AddressMaps([addressMap]); + + const unsignedTx = new UnsignedTx( + importTx, + [], // Empty UTXOs array, will be filled during processing + addressMaps, + credentials + ); + + this.transaction.setTransaction(unsignedTx); + } + + /** + * Create inputs from UTXOs + * @return { + * inputs: TransferableInput[]; + * credentials: Credential[]; + * amount: bigint; + * } + */ + protected createInputs(): { + inputs: TransferableInput[]; + credentials: Credential[]; + amount: bigint; + } { + const sender = this.transaction._fromAddresses.slice(); + if (this.recoverSigner) { + // switch first and last signer + const tmp = sender.pop(); + sender.push(sender[0]); + if (tmp) { + sender[0] = tmp; + } + } + + let totalAmount = BigInt(0); + const inputs: TransferableInput[] = []; + const credentials: Credential[] = []; + + this.transaction._utxos.forEach((utxo) => { + const amount = BigInt(utxo.amount); + totalAmount += amount; + + // Create input with proper interface implementation + const input = { + _type: TypeSymbols.Input, + amount: () => amount, + sigIndices: sender.map((_, i) => i), + toBytes: () => new Uint8Array(), + }; + + // Create TransferableInput with proper UTXOID implementation + const txId = new Id(new Uint8Array(Buffer.from(utxo.txid, 'hex'))); + const outputIdxInt = new Int(Number(utxo.outputidx)); + const outputIdxBytes = new Uint8Array(Buffer.alloc(4)); + new DataView(outputIdxBytes.buffer).setInt32(0, Number(utxo.outputidx), true); + const outputIdxId = new Id(outputIdxBytes); + + // Create asset with complete Amounter interface + const assetIdBytes = new Uint8Array(Buffer.from(this.transaction._assetId, 'hex')); + const assetId = { + _type: TypeSymbols.BaseTx, + amount: () => amount, + toBytes: () => assetIdBytes, + toString: () => Buffer.from(assetIdBytes).toString('hex'), + }; + + // Create TransferableInput with UTXOID using Int for outputIdx + const transferableInput = new TransferableInput( + { + _type: TypeSymbols.UTXOID, + txID: txId, + outputIdx: outputIdxInt, + ID: () => utxo.txid, + toBytes: () => new Uint8Array(), + }, + outputIdxId, // Use Id type for TransferableInput constructor + assetId // Use asset with complete Amounter interface + ); + + // Set input properties + Object.assign(transferableInput, { input }); + inputs.push(transferableInput); + + // Create empty credential for each input + const emptySignatures = sender.map(() => utils.createNewSig('')); + const credential = new Credential(emptySignatures); + credentials.push(credential); + }); + + return { + inputs, + credentials, + amount: totalAmount, + }; + } + + /** + * Calculate the fee size for the transaction + */ + private calculateFeeSize(tx?: evmSerial.ImportTx): number { + // If tx is provided, calculate based on actual transaction size + if (tx) { + const codec = avmSerial.getAVMManager().getDefaultCodec(); + return tx.toBytes(codec).length; + } + + // Otherwise estimate based on typical import transaction size + const baseSize = 256; // Base transaction size + const inputSize = 128; // Size per input + const outputSize = 64; // Size per output + const numInputs = this.transaction._utxos.length; + const numOutputs = 1; // Import tx always has 1 output + + return baseSize + inputSize * numInputs + outputSize * numOutputs; + } + + /** + * Recover UTXOs from imported inputs + * @param importedInputs Array of transferable inputs + * @returns Array of decoded UTXO objects + */ + private recoverUtxos(importedInputs: TransferableInput[]): DecodedUtxoObj[] { + return importedInputs.map((input) => { + const txid = input.utxoID.toString(); + const outputidx = input.utxoID.outputIdx.toString(); + + return { + outputID: SECP256K1_Transfer_Output, + amount: input.amount().toString(), + txid: utils.cb58Encode(Buffer.from(txid, 'hex')), + outputidx: outputidx, + threshold: this.transaction._threshold, + addresses: this.transaction._fromAddresses.map((addr) => + utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr)) + ), + }; + }); + } +} diff --git a/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts new file mode 100644 index 0000000000..5122fbcd92 --- /dev/null +++ b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts @@ -0,0 +1,141 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core'; +import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; +import { + evmSerial, + UnsignedTx, + Int, + Id, + TransferableInput, + utils as FlareUtils, + Address, + BigIntPr, +} from '@flarenetwork/flarejs'; +import utils from './utils'; +import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface'; + +export class ImportInPTxBuilder extends AtomicTransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + // external chain id is P + this._externalChainId = utils.cb58Decode(this.transaction._network.blockchainID); + // chain id is C + this.transaction._blockchainID = Buffer.from( + utils.cb58Decode(this.transaction._network.cChainBlockchainID) + ).toString('hex'); + } + + protected get transactionType(): TransactionType { + return TransactionType.Import; + } + + initBuilder(tx: Tx): this { + const baseTx = tx as evmSerial.ImportTx; + if (!this.verifyTxType(baseTx._type)) { + throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type'); + } + + // The regular change output is the tx output in Import tx. + // createInputOutput results in a single item array. + // It's expected to have only one output with the addresses of the sender. + const outputs = baseTx.Outs; + if (outputs.length !== 1) { + throw new BuildTransactionError('Transaction can have one external output'); + } + + const output = outputs[0]; + const assetId = output.assetId.toBytes(); + if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId)) !== 0) { + throw new Error('The Asset ID of the output does not match the transaction'); + } + + // Set locktime to 0 since it's not used in EVM outputs + this.transaction._locktime = BigInt(0); + + // Set threshold to 1 since EVM outputs only have one address + this.transaction._threshold = 1; + + // Convert output address to buffer and set as fromAddress + this.transaction._fromAddresses = [Buffer.from(output.address.toBytes())]; + + // Set external chain ID from the source chain + this._externalChainId = Buffer.from(baseTx.sourceChain.toString()); + + // Recover UTXOs from imported inputs + this.transaction._utxos = this.recoverUtxos(baseTx.importedInputs); + + return this; + } + + static verifyTxType(txnType: string): boolean { + return txnType === FlareTransactionType.PvmImportTx; + } + + verifyTxType(txnType: string): boolean { + return ImportInPTxBuilder.verifyTxType(txnType); + } + + /** + * Build the import transaction + * @protected + */ + protected buildFlareTransaction(): void { + // if tx has credentials, tx shouldn't change + if (this.transaction.hasCredentials) return; + + const { inputs, credentials } = this.createInputOutput(BigInt(this.transaction.fee.fee)); + + // Convert TransferableInput to evmSerial.Output + const evmOutputs = inputs.map((input) => { + return new evmSerial.Output( + new Address(this.transaction._fromAddresses[0]), + new BigIntPr(input.input.amount()), + new Id(input.assetId.toBytes()) + ); + }); + + // Create the import transaction + const importTx = new evmSerial.ImportTx( + new Int(this.transaction._networkID), + Id.fromString(this.transaction._blockchainID.toString()), + Id.fromString(this._externalChainId.toString()), + inputs, + evmOutputs + ); + + const addressMaps = this.transaction._fromAddresses.map((a) => new FlareUtils.AddressMap([[new Address(a), 0]])); + + // Create unsigned transaction + const unsignedTx = new UnsignedTx( + importTx, + [], // Empty UTXOs array, will be filled during processing + new FlareUtils.AddressMaps(addressMaps), + credentials + ); + + this.transaction.setTransaction(unsignedTx); + } + + /** + * Recover UTXOs from imported inputs + * @param importedInputs Array of transferable inputs + * @returns Array of decoded UTXO objects + */ + private recoverUtxos(importedInputs: TransferableInput[]): DecodedUtxoObj[] { + return importedInputs.map((input) => { + const utxoId = input.utxoID; + const transferInput = input.input; + const utxo: DecodedUtxoObj = { + outputID: SECP256K1_Transfer_Output, + amount: transferInput.amount.toString(), + txid: utils.cb58Encode(Buffer.from(utxoId.ID.toString())), + outputidx: utxoId.outputIdx.toBytes().toString(), + threshold: this.transaction._threshold, + addresses: this.transaction._fromAddresses.map((addr) => + utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr)) + ), + }; + return utxo; + }); + } +} diff --git a/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts index 7f26bd836d..e0b53de831 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicInCTransactionBuilder.ts @@ -1,74 +1,69 @@ import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import utils from './utils'; import { BuildTransactionError } from '@bitgo/sdk-core'; +import { evmSerial, UnsignedTx, utils as FlareUtils, avmSerial, Address } from '@flarenetwork/flarejs'; +import utils from './utils'; +import { Transaction } from './transaction'; -interface FlareChainNetworkMeta { - blockchainID?: string; // P-chain id (external) - cChainBlockchainID?: string; // C-chain id (local) - [k: string]: unknown; -} - -interface FeeShape { - fee?: string; // legacy - feeRate?: string; // per unit rate -} - -/** - * Flare P->C atomic import/export style builder (C-chain context). This adapts the AVAXP logic - * removing direct Avalanche SDK dependencies. Network / chain ids are expected to be provided - * in the transaction._network object by a higher-level factory once Flare network constants - * are finalized. For now we CB58-decode placeholders if present and default to zero buffers. - */ export abstract class AtomicInCTransactionBuilder extends AtomicTransactionBuilder { - // Placeholder fixed fee (can be overridden by subclasses or network config) - protected fixedFee = 0n; constructor(_coinConfig: Readonly) { super(_coinConfig); - this.initializeChainIds(); + // external chain id is P + this._externalChainId = utils.cb58Decode(this.transaction._network.blockchainID); + // chain id is C + this.transaction._blockchainID = Buffer.from( + utils.cb58Decode(this.transaction._network.cChainBlockchainID) + ).toString('hex'); } /** - * Set base fee (already scaled to Flare C-chain native decimals). Accept bigint | number | string. + * C-Chain base fee with decimal places converted from 18 to 9. + * + * @param {string | number} baseFee */ - feeRate(baseFee: bigint | number | string): this { - const n = typeof baseFee === 'bigint' ? baseFee : BigInt(baseFee); - this.validateFee(n); - this.setFeeRate(n); + feeRate(baseFee: string | number): this { + const fee = BigInt(baseFee); + this.validateFee(fee); + this.transaction._fee.feeRate = Number(fee); return this; } - /** - * Recreate builder state from raw tx (hex). Flare C-chain support TBD; for now validate & stash. - */ - protected fromImplementation(rawTransaction: string): { _tx?: unknown } { - // 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); + /** @inheritdoc */ + fromImplementation(rawTransaction: string): Transaction { + const txBytes = new Uint8Array(Buffer.from(rawTransaction, 'hex')); + const codec = avmSerial.getAVMManager().getDefaultCodec(); + const [tx] = evmSerial.ImportTx.fromBytes(txBytes, codec); + + const addressMaps = this.transaction._fromAddresses.map((a) => new FlareUtils.AddressMap([[new Address(a), 0]])); + + const unsignedTx = new UnsignedTx(tx, [], new FlareUtils.AddressMaps(addressMaps), []); + this.initBuilder(unsignedTx); return this.transaction; } - private validateFee(fee: bigint): void { - if (fee <= 0n) { + /** + * Check that fee is greater than 0. + * @param {bigint} fee + */ + validateFee(fee: bigint): void { + if (fee <= BigInt(0)) { throw new BuildTransactionError('Fee must be greater than 0'); } } - 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); + /** + * Initialize the transaction builder fields using the decoded transaction data + * + * @param {UnsignedTx} tx the transaction data + * @returns itself + */ + initBuilder(tx: UnsignedTx): this { + // Validate network and blockchain IDs + const baseTx = tx.getTx(); + if (baseTx.getBlockchainId() !== this.transaction._blockchainID) { + throw new Error('blockchain ID mismatch'); } - } - - private setFeeRate(n: bigint): void { - const currentContainer = this.transaction as unknown as { _fee: FeeShape }; - const current = currentContainer._fee || { fee: '0' }; - currentContainer._fee = { ...current, feeRate: n.toString() }; + this.transaction.setTransaction(tx); + return this; } } diff --git a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts index 97abda8650..b516762b10 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts @@ -1,423 +1,192 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType, BaseTransaction } from '@bitgo/sdk-core'; -import { Credential, Signature, TransferableInput, TransferableOutput } from '@flarenetwork/flarejs'; -import { TransactionExplanation, DecodedUtxoObj } from './iface'; -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, - ERROR_CREDENTIALS_ARRAY, - ERROR_UTXOS_REQUIRED, - 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'; - -/** - * 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 { - 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; - }, - }; - - constructor(coinConfig: Readonly) { - this._coinConfig = coinConfig; - } - - protected abstract get transactionType(): TransactionType; - - /** - * Get the asset ID for Flare network transactions - * @returns Buffer containing the asset ID - */ - 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; - } - - // For native FLR transactions, return zero-filled buffer as placeholder - // In a real implementation, this would be obtained from the network configuration - // or FlareJS API to get the actual native asset ID - return Buffer.alloc(ASSET_ID_LENGTH); - } - - validateAmount(amount: bigint): void { - if (amount <= ZERO_BIGINT) { - throw new BuildTransactionError(ERROR_AMOUNT_POSITIVE); - } - } +import { TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; +import { TransferableInput, Int, Id, TypeSymbols } from '@flarenetwork/flarejs'; +import { DecodedUtxoObj } from './iface'; + +// Interface for objects that can provide an amount +interface Amounter { + _type: TypeSymbols; + amount: () => bigint; + toBytes: () => Uint8Array; +} - /** - * Validates that credentials array is properly formed - * @param credentials - Array of credentials to validate - */ - protected validateCredentials(credentials: Credential[]): void { - if (!Array.isArray(credentials)) { - throw new BuildTransactionError(ERROR_CREDENTIALS_ARRAY); - } +export abstract class AtomicTransactionBuilder extends TransactionBuilder { + protected _externalChainId: Buffer; + protected recoverSigner = false; - credentials.forEach((credential, index) => { - if (!(credential instanceof Credential)) { - throw new BuildTransactionError(`Invalid credential at index ${index}`); - } - }); + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this.transaction = new Transaction(_coinConfig); } /** - * Creates inputs, outputs, and credentials for Flare P-chain atomic transactions. - * Based on AVAX P-chain implementation adapted for FlareJS. - * - * Note: This is a simplified implementation that creates the core structure. - * The FlareJS type system integration will be refined in future iterations. - * - * @param total - Total amount needed including fees - * @returns Object containing TransferableInput[], TransferableOutput[], and Credential[] + * Create inputs and outputs from UTXOs + * @param {bigint} amount Amount to transfer + * @return { + * inputs: TransferableInput[]; + * outputs: TransferableInput[]; + * credentials: Credential[]; + * } + * @protected */ - protected createInputOutput(total: bigint): { + protected createInputOutput(amount: bigint): { inputs: TransferableInput[]; - outputs: TransferableOutput[]; - credentials: Credential[]; + outputs: TransferableInput[]; + credentials: any[]; } { - if (!this._utxos || this._utxos.length === ZERO_NUMBER) { - throw new BuildTransactionError(ERROR_UTXOS_REQUIRED); + const sender = (this.transaction as Transaction)._fromAddresses.slice(); + if (this.recoverSigner) { + // switch first and last signer + const tmp = sender.pop(); + sender.push(sender[0]); + if (tmp) { + sender[0] = tmp; + } } + let totalAmount = BigInt(0); const inputs: TransferableInput[] = []; - const outputs: TransferableOutput[] = []; - const credentials: Credential[] = []; - - let inputSum = 0n; - const addressIndices: { [address: string]: number } = {}; - let nextAddressIndex = 0; - - // Sort UTXOs by amount in descending order for optimal coin selection - const sortedUtxos = [...this._utxos].sort((a, b) => { - const amountA = BigInt(a.amount); - const amountB = BigInt(b.amount); - if (amountA > amountB) return -1; - if (amountA < amountB) return 1; - return 0; - }); + const outputs: TransferableInput[] = []; + const credentials: any[] = []; - // Process UTXOs to create inputs and credentials - for (const utxo of sortedUtxos) { + (this.transaction as Transaction)._utxos.forEach((utxo: DecodedUtxoObj) => { const utxoAmount = BigInt(utxo.amount); + totalAmount += utxoAmount; + + // Create input + const input = { + _type: TypeSymbols.Input, + amount: () => utxoAmount, + sigIndices: sender.map((_, i) => i), + toBytes: () => new Uint8Array(), + }; - if (inputSum >= total) { - break; // We have enough inputs - } - - // Track input sum - inputSum += utxoAmount; - - // Track address indices for signature ordering (mimics AVAX pattern) - const addressIndexArray: number[] = []; - for (const address of utxo.addresses) { - if (!(address in addressIndices)) { - addressIndices[address] = nextAddressIndex++; - } - addressIndexArray.push(addressIndices[address]); - } - - // Store address indices on the UTXO for credential creation - utxo.addressesIndex = addressIndexArray; - - // Create TransferableInput for atomic transactions - const transferableInput = { - txID: Buffer.from(utxo.txid || AMOUNT_STRING_ZERO.repeat(TRANSACTION_ID_HEX_LENGTH), HEX_ENCODING), - outputIndex: parseInt(utxo.outputidx || AMOUNT_STRING_ZERO, DECIMAL_RADIX), - assetID: this.getAssetId(), - input: { - amount: utxoAmount, - addressIndices: addressIndexArray, - threshold: utxo.threshold, + // Create asset with Amounter interface + const assetId: Amounter = { + _type: TypeSymbols.BaseTx, + amount: () => utxoAmount, + toBytes: () => { + const bytes = new Uint8Array(Buffer.from((this.transaction as Transaction)._assetId, 'hex')); + return bytes; }, }; - // Store the input (type assertion for compatibility) - inputs.push(transferableInput as unknown as TransferableInput); + // Create TransferableInput + const transferableInput = new TransferableInput( + { + _type: TypeSymbols.UTXOID, + txID: new Id(new Uint8Array(Buffer.from(utxo.txid, 'hex'))), + outputIdx: new Int(Number(utxo.outputidx)), + ID: () => utxo.txid, + toBytes: () => { + const txIdBytes = new Uint8Array(Buffer.from(utxo.txid, 'hex')); + const outputIdxBytes = new Uint8Array(4); + new DataView(outputIdxBytes.buffer).setInt32(0, Number(utxo.outputidx), true); + return Buffer.concat([txIdBytes, outputIdxBytes]); + }, + }, + new Id(new Uint8Array(Buffer.from(utxo.outputidx.toString()))), + assetId + ); - // Create credential with placeholder signatures - // In a real implementation, these would be actual signatures - const signatures = Array.from({ length: utxo.threshold }, () => ''); - const credential = this.createFlareCredential(0, signatures); - credentials.push(credential); - } + // Set input properties + Object.assign(transferableInput, { input }); + inputs.push(transferableInput); - // Verify we have enough inputs - if (inputSum < total) { - throw new BuildTransactionError(`Insufficient funds: need ${total}, have ${inputSum}`); - } + // Create empty credential for each input + const emptySignatures = sender.map(() => Buffer.alloc(0)); + credentials.push({ signatures: emptySignatures }); + }); - // Create change output if we have excess input amount - if (inputSum > total) { - const changeAmount = inputSum - total; + // Create output if there is change + if (totalAmount > amount) { + const changeAmount = totalAmount - amount; + const output = { + _type: TypeSymbols.BaseTx, + amount: () => changeAmount, + addresses: sender, + locktime: (this.transaction as Transaction)._locktime, + threshold: (this.transaction as Transaction)._threshold, + toBytes: () => new Uint8Array(), + }; - // Create change output for atomic transactions - const changeOutput = { - assetID: this.getAssetId(), - output: { - amount: changeAmount, - addresses: this.transaction._fromAddresses, - threshold: 1, - locktime: 0n, + // Create asset with Amounter interface + const assetId: Amounter = { + _type: TypeSymbols.BaseTx, + amount: () => changeAmount, + toBytes: () => { + const bytes = new Uint8Array(Buffer.from((this.transaction as Transaction)._assetId, 'hex')); + return bytes; }, }; - // Add the change output (type assertion for compatibility) - outputs.push(changeOutput as unknown as TransferableOutput); - } - - return { inputs, outputs, credentials }; - } - - /** - * Set UTXOs for the transaction. This is required for creating inputs and outputs. - * - * @param utxos - Array of decoded UTXO objects - * @returns this builder instance for chaining - */ - utxos(utxos: DecodedUtxoObj[]): this { - this._utxos = utxos; - return this; - } - - /** - * Flare equivalent of Avalanche's SelectCredentialClass - * Creates a credential with the provided signatures - * - * @param credentialId - The credential ID (not used in FlareJS but kept for compatibility) - * @param signatures - Array of signature hex strings or empty strings for placeholders - * @returns Credential instance - */ - protected createFlareCredential(_credentialId: number, signatures: string[]): Credential { - if (!Array.isArray(signatures)) { - throw new BuildTransactionError(ERROR_SIGNATURES_ARRAY); - } + // Create TransferableOutput + const transferableOutput = new TransferableInput( + { + _type: TypeSymbols.UTXOID, + txID: new Id(new Uint8Array(32)), + outputIdx: new Int(0), + ID: () => '', + toBytes: () => { + const txIdBytes = new Uint8Array(32); + const outputIdxBytes = new Uint8Array(4); + return Buffer.concat([txIdBytes, outputIdxBytes]); + }, + }, + new Id(new Uint8Array([0])), + assetId + ); - if (signatures.length === ZERO_NUMBER) { - throw new BuildTransactionError(ERROR_SIGNATURES_EMPTY); + // Set output properties + Object.assign(transferableOutput, { output }); + outputs.push(transferableOutput); } - const sigs = signatures.map((sig, index) => { - // Handle empty/placeholder signatures - if (!sig || sig.length === 0) { - return new Signature(new Uint8Array(SECP256K1_SIGNATURE_LENGTH)); - } - - // Validate hex string format - const cleanSig = sig.startsWith(HEX_PREFIX) ? sig.slice(HEX_PREFIX_LENGTH) : sig; - if (!createFlexibleHexRegex().test(cleanSig)) { - throw new BuildTransactionError(`Invalid hex signature at index ${index}: contains non-hex characters`); - } - - // Convert to buffer and validate length - const sigBuffer = Buffer.from(cleanSig, HEX_ENCODING); - if (sigBuffer.length > SECP256K1_SIGNATURE_LENGTH) { - throw new BuildTransactionError( - `Signature too long at index ${index}: ${sigBuffer.length} bytes (max ${SECP256K1_SIGNATURE_LENGTH})` - ); - } - - // Create fixed-length buffer and copy signature data - const fixedLengthBuffer = Buffer.alloc(SECP256K1_SIGNATURE_LENGTH); - sigBuffer.copy(fixedLengthBuffer); + return { + inputs, + outputs, + credentials, + }; + } - try { - return new Signature(new Uint8Array(fixedLengthBuffer)); - } catch (error) { - throw new BuildTransactionError( - `Failed to create signature at index ${index}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` - ); + /** @inheritdoc */ + protected async buildImplementation(): Promise { + this.buildFlareTransaction(); + this.setTransactionType(this.transactionType); + if (this.hasSigner()) { + // Sign sequentially to ensure proper order + for (const keyPair of this._signer) { + await this.transaction.sign(keyPair); } - }); - - try { - return new Credential(sigs); - } catch (error) { - throw new BuildTransactionError( - `${ERROR_CREATE_CREDENTIAL_FAILED}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` - ); } + return this.transaction; } /** - * Base initBuilder used by concrete builders. For now just returns this so fluent API works. - */ - initBuilder(_tx: unknown): this { - return this; - } - - /** - * Sign transaction with private key using FlareJS compatibility + * Builds the avax transaction. transaction field is changed. */ - sign(params: { key: string }): this { - // FlareJS signing implementation with atomic transaction support - try { - // Validate private key format (placeholder implementation) - if (!params.key || params.key.length < PRIVATE_KEY_HEX_LENGTH) { - throw new BuildTransactionError(ERROR_INVALID_PRIVATE_KEY); - } - - // Create signature structure - const signature = { - privateKey: params.key, - signingMethod: SIGNING_METHOD, - }; + protected abstract buildFlareTransaction(): void; - // Store signature for FlareJS compatibility - this.transaction._signature = signature; - this.transaction.hasCredentials = true; - - return this; - } catch (error) { - throw new BuildTransactionError( - `${ERROR_FLAREJS_SIGNING_FAILED}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` - ); - } - } + protected abstract get transactionType(): TransactionType; /** - * Build the transaction using FlareJS compatibility + * Fee is fix for AVM atomic tx. + * + * @returns network.txFee + * @protected */ - 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}` - ); - } + protected get fixedFee(): string { + return this.transaction._network.txFee; } /** - * Parse and explain a transaction from hex using FlareJS compatibility + * Set the transaction type + * + * @param {TransactionType} transactionType The transaction type to be set */ - explainTransaction(): TransactionExplanation { - // FlareJS transaction parsing with atomic support - try { - return { - type: this.transactionType, - inputs: [], - outputs: [], - outputAmount: AMOUNT_STRING_ZERO, - rewardAddresses: [], - id: `${FLARE_ATOMIC_PARSED_PREFIX}${Date.now()}`, - changeOutputs: [], - changeAmount: AMOUNT_STRING_ZERO, - fee: { fee: this.transaction._fee.fee }, - }; - } catch (error) { - throw new BuildTransactionError( - `${ERROR_ENHANCED_PARSE_FAILED}: ${error instanceof Error ? error.message : ERROR_UNKNOWN}` - ); - } + setTransactionType(transactionType: TransactionType): void { + this.transaction._type = transactionType; } } diff --git a/modules/sdk-coin-flrp/src/lib/constants.ts b/modules/sdk-coin-flrp/src/lib/constants.ts deleted file mode 100644 index 447dc92b43..0000000000 --- a/modules/sdk-coin-flrp/src/lib/constants.ts +++ /dev/null @@ -1,256 +0,0 @@ -// Shared constants for Flare P-Chain (flrp) utilities and key handling. -// Centralizing avoids magic numbers scattered across utils and keyPair implementations. - -export const DECODED_BLOCK_ID_LENGTH = 32; // Expected decoded block identifier length -export const SHORT_PUB_KEY_LENGTH = 50; // Placeholder (potential CB58 encoded form length) -export const COMPRESSED_PUBLIC_KEY_LENGTH = 66; // 33 bytes (compressed) hex encoded -export const UNCOMPRESSED_PUBLIC_KEY_LENGTH = 130; // 65 bytes (uncompressed) hex encoded -export const RAW_PRIVATE_KEY_LENGTH = 64; // 32 bytes hex encoded -export const SUFFIXED_PRIVATE_KEY_LENGTH = 66; // 32 bytes + compression flag suffix -export const PRIVATE_KEY_COMPRESSED_SUFFIX = '01'; -export const OUTPUT_INDEX_HEX_LENGTH = 8; // 4 bytes serialized to hex length - -// Asset and transaction constants -export const ASSET_ID_LENGTH = 32; // Asset ID length in bytes (standard for AVAX/Flare networks) -export const TRANSACTION_ID_HEX_LENGTH = 64; // Transaction ID length in hex characters (32 bytes) -export const PRIVATE_KEY_HEX_LENGTH = 64; // Private key length in hex characters (32 bytes) -export const SECP256K1_SIGNATURE_LENGTH = 65; // SECP256K1 signature length in bytes -export const BLS_PUBLIC_KEY_COMPRESSED_LENGTH = 96; // BLS public key compressed length in hex chars (48 bytes) -export const BLS_PUBLIC_KEY_UNCOMPRESSED_LENGTH = 192; // BLS public key uncompressed length in hex chars (96 bytes) -export const BLS_SIGNATURE_LENGTH = 192; // BLS signature length in hex characters (96 bytes) -export const CHAIN_ID_HEX_LENGTH = 64; // Chain ID length in hex characters (32 bytes) -export const MAX_CHAIN_ID_LENGTH = 128; // Maximum chain ID string length - -// Fee constants (in nanoFLR) -export const DEFAULT_BASE_FEE = '1000000'; // 1M nanoFLR default base fee -export const DEFAULT_EVM_GAS_FEE = '21000'; // Standard EVM transfer gas fee -export const INPUT_FEE = '100000'; // 100K nanoFLR per input (FlareJS standard) -export const OUTPUT_FEE = '50000'; // 50K nanoFLR per output (FlareJS standard) -export const MINIMUM_FEE = '1000000'; // 1M nanoFLR minimum fee - -// Validator constants -export const MIN_DELEGATION_FEE_BASIS_POINTS = 20000; // 2% minimum delegation fee -export const BASIS_POINTS_DIVISOR = 10000; // Divisor to convert basis points to decimal -export const PERCENTAGE_MULTIPLIER = 100; // Multiplier to convert decimal to percentage - -// Transaction ID prefix -export const TRANSACTION_ID_PREFIX = 'flare-atomic-tx-'; // Prefix for transaction IDs - -// Transaction type constants -export const DELEGATOR_TRANSACTION_TYPE = 'PlatformVM.AddDelegatorTx'; // Delegator transaction type - -// Delegator type constants -export const PRIMARY_DELEGATOR_TYPE = 'primary'; // Primary delegator type -export const DELEGATOR_STAKE_TYPE = 'delegator'; // Delegator stake type -export const SECP256K1_CREDENTIAL_TYPE = 'secp256k1fx.Credential'; // SECP256K1 credential type -export const STAKE_OUTPUT_TYPE = 'stake'; // Stake output type -export const CREDENTIAL_VERSION = '1.0.0'; // Credential version - -// Default values and thresholds -export const EMPTY_STRING = ''; // Empty string default -export const ZERO_BIGINT = 0n; // Zero BigInt default -export const ZERO_NUMBER = 0; // Zero number default -export const DEFAULT_THRESHOLD = 1; // Default signature threshold -export const DEFAULT_LOCKTIME = 0n; // Default locktime -export const MEMO_BUFFER_SIZE = 0; // Empty memo buffer size -export const FIRST_ADDRESS_INDEX = 0; // First address index - -// Regex patterns -export const ADDRESS_REGEX = /^(^P||NodeID)-[a-zA-Z0-9]+$/; -export const HEX_REGEX = /^(0x){0,1}([0-9a-f])+$/i; - -// Hex pattern components for building dynamic regexes -export const HEX_CHAR_PATTERN = '[0-9a-fA-F]'; -export const HEX_PATTERN_NO_PREFIX = `^${HEX_CHAR_PATTERN}*$`; -export const HEX_PATTERN_WITH_PREFIX = `^0x${HEX_CHAR_PATTERN}`; - -// Network and buffer constants -export const DEFAULT_NETWORK_ID = 0; // Default network ID -export const FLARE_MAINNET_NETWORK_ID = 1; // Flare mainnet network ID -export const FLARE_TESTNET_NETWORK_ID = 5; // Flare testnet network ID -export const EMPTY_BUFFER_SIZE = 0; // Empty buffer allocation size -export const HEX_PREFIX = '0x'; // Hex prefix -export const HEX_PREFIX_LENGTH = 2; // Length of hex prefix -export const DECIMAL_RADIX = 10; // Decimal radix for parseInt -export const SIGNING_METHOD = 'secp256k1'; // Default signing method -export const AMOUNT_STRING_ZERO = '0'; // Zero amount as string -export const FIRST_ARRAY_INDEX = 0; // First array index -export const MAINNET_TYPE = 'mainnet'; // Mainnet type string - -// Transaction type constants for export -export const EXPORT_TRANSACTION_TYPE = 'PlatformVM.ExportTx'; // Export transaction type - -// Error messages -export const ERROR_AMOUNT_POSITIVE = 'Amount must be positive'; -export const ERROR_CREDENTIALS_ARRAY = 'Credentials must be an array'; -export const ERROR_UTXOS_REQUIRED = 'UTXOs are required for creating inputs and outputs'; -export const ERROR_SIGNATURES_ARRAY = 'Signatures must be an array'; -export const ERROR_SIGNATURES_EMPTY = 'Signatures array cannot be empty'; -export const ERROR_INVALID_PRIVATE_KEY = 'Invalid private key format'; -export const ERROR_UTXOS_REQUIRED_BUILD = 'UTXOs are required for transaction building'; -export const ERROR_ENHANCED_BUILD_FAILED = 'Enhanced FlareJS transaction building failed'; -export const ERROR_ENHANCED_PARSE_FAILED = 'Enhanced FlareJS transaction parsing failed'; -export const ERROR_FLAREJS_SIGNING_FAILED = 'FlareJS signing failed'; -export const ERROR_CREATE_CREDENTIAL_FAILED = 'Failed to create credential'; -export const ERROR_UNKNOWN = 'unknown error'; -export const ERROR_EXPORT_NOT_IMPLEMENTED = 'Flare P-chain export transaction build not implemented'; -export const ERROR_DESTINATION_CHAIN_REQUIRED = 'Destination chain ID must be set for P-chain export'; -export const ERROR_SOURCE_ADDRESSES_REQUIRED = 'Source addresses must be set for P-chain export'; -export const ERROR_DESTINATION_ADDRESSES_REQUIRED = 'Destination addresses must be set for P-chain export'; -export const ERROR_EXPORT_AMOUNT_POSITIVE = 'Export amount must be positive'; -export const ERROR_TRANSACTION_REQUIRED = 'Transaction is required for initialization'; -export const ERROR_BLOCKCHAIN_ID_MISMATCH = 'Blockchain ID mismatch'; -export const ERROR_TRANSACTION_PARSE_FAILED = 'Transaction cannot be parsed or has an unsupported transaction type'; -export const ERROR_FAILED_INITIALIZE_BUILDER = 'Failed to initialize builder from transaction'; - -// Type checking constants -export const OBJECT_TYPE_STRING = 'object'; // Object type string for typeof checks -export const STRING_TYPE = 'string'; // String type for typeof checks -export const NUMBER_TYPE = 'number'; // Number type for typeof checks -export const FUNCTION_TYPE = 'function'; // Function type for typeof checks -export const BIGINT_TYPE = 'bigint'; // BigInt type for typeof checks -export const HEX_ENCODING = 'hex'; // Hex encoding string -export const UTF8_ENCODING = 'utf8'; // UTF8 encoding string - -// Chain identifiers -export const P_CHAIN = 'P'; // P-chain identifier -export const C_CHAIN = 'C'; // C-chain identifier -export const X_CHAIN = 'X'; // X-chain identifier -export const P_CHAIN_FULL = 'P-chain'; // P-chain full name -export const C_CHAIN_FULL = 'C-chain'; // C-chain full name -export const X_CHAIN_FULL = 'X-chain'; // X-chain full name -export const CHAIN_SUFFIX = '-chain'; // Chain name suffix - -// Atomic transaction prefixes -export const FLARE_ATOMIC_PREFIX = 'flare-atomic-'; // Prefix for atomic transaction IDs -export const FLARE_ATOMIC_PARSED_PREFIX = 'flare-atomic-parsed-'; // Prefix for parsed transaction IDs - -// Placeholder values -export const FLARE_ADDRESS_PLACEHOLDER = 'flare-address-placeholder'; // Placeholder for address conversion -export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; // Zero address -export const HEX_PREFIX_REMOVE = '0x'; // Hex prefix to remove -export const PADSTART_CHAR = '0'; // Character used for padding - -// FlareJS transaction types -export const PLATFORM_VM_IMPORT_TX = 'PlatformVM.ImportTx'; // Platform VM import transaction -export const PLATFORM_VM_ADD_VALIDATOR_TX = 'PlatformVM.AddValidatorTx'; // Platform VM add validator transaction -export const IMPORT_TX_TYPE = 'ImportTx'; // Import transaction type -export const P_CHAIN_IMPORT_TYPE = 'P-chain-import'; // P-chain import type - -// Transaction types and values -export const IMPORT_TYPE = 'import'; // Import transaction type -export const EXPORT_TYPE = 'export'; // Export transaction type -export const SEND_TYPE = 'send'; // Send transaction type -export const IMPORT_C_TYPE = 'import-c'; // Import C-chain transaction type -export const VALIDATOR_TYPE = 'validator'; // Validator type -export const ADDVALIDATOR_TYPE = 'addValidator'; // Add validator type -export const ADD_VALIDATOR_TX_TYPE = 'AddValidatorTx'; // Add validator transaction type - -// Validator transaction type arrays -export const VALIDATOR_TRANSACTION_TYPES = [ - PLATFORM_VM_ADD_VALIDATOR_TX, - ADD_VALIDATOR_TX_TYPE, - ADDVALIDATOR_TYPE, - VALIDATOR_TYPE, -]; // Valid validator transaction types - -// Transfer types -export const TRANSFERABLE_INPUT_TYPE = 'TransferableInput'; // Transferable input type -export const CREDENTIAL_TYPE = 'Credential'; // Credential type -export const SECP256K1_TRANSFER_INPUT_TYPE = 'secp256k1fx.TransferInput'; // SECP256K1 transfer input type -export const SECP256K1_TRANSFER_OUTPUT_TYPE = 'secp256k1fx.TransferOutput'; // SECP256K1 transfer output type - -// Property names for object checks -export const DESTINATION_CHAIN_PROP = 'destinationChain'; // Destination chain property -export const DESTINATION_CHAIN_ID_PROP = 'destinationChainID'; // Destination chain ID property -export const EXPORTED_OUTPUTS_PROP = 'exportedOutputs'; // Exported outputs property -export const OUTS_PROP = 'outs'; // Outputs property short name -export const INPUTS_PROP = 'inputs'; // Inputs property -export const INS_PROP = 'ins'; // Inputs property short name -export const NETWORK_ID_PROP = 'networkID'; // Network ID property -export const NETWORK_ID_PROP_ALT = 'networkId'; // Alternative network ID property -export const BLOCKCHAIN_ID_PROP = 'blockchainID'; // Blockchain ID property -export const GET_OUTPUT_METHOD = 'getOutput'; // Get output method name - -// UTXO field names -export const OUTPUT_ID_FIELD = 'outputID'; // Output ID field -export const AMOUNT_FIELD = 'amount'; // Amount field -export const TXID_FIELD = 'txid'; // Transaction ID field -export const OUTPUT_IDX_FIELD = 'outputidx'; // Output index field - -// Transaction explanation field names -export const ID_FIELD = 'id'; // ID field name -export const OUTPUT_AMOUNT_FIELD = 'outputAmount'; // Output amount field -export const CHANGE_AMOUNT_FIELD = 'changeAmount'; // Change amount field -export const OUTPUTS_FIELD = 'outputs'; // Outputs field -export const CHANGE_OUTPUTS_FIELD = 'changeOutputs'; // Change outputs field -export const FEE_FIELD = 'fee'; // Fee field -export const TYPE_FIELD = 'type'; // Type field - -// Signature and hash methods -export const SECP256K1_SIG_TYPE = 'secp256k1'; // SECP256K1 signature type -export const DER_FORMAT = 'der'; // DER format -export const SHA256_HASH = 'sha256'; // SHA256 hash function -export const RECOVERY_KEY_METHOD = 'recovery-key'; // Recovery key signing method -export const NORMAL_MODE = 'normal'; // Normal mode -export const RECOVERY_MODE = 'recovery'; // Recovery mode - -// Version strings -export const RECOVERY_VERSION = '1.0.0'; // Recovery version -export const SIGNATURE_VERSION = '1.0.0'; // Signature version - -// Numeric radix -export const HEX_RADIX = 16; // Hexadecimal radix - -// Transaction type identifiers (additional) -export const ADD_PERMISSIONLESS_VALIDATOR_TYPE = 'addPermissionlessValidator'; // Add permissionless validator type - -// Display order for transaction explanations -export const DISPLAY_ORDER_BASE = [ - 'id', - 'inputs', - 'outputAmount', - 'changeAmount', - 'outputs', - 'changeOutputs', - 'fee', - 'type', -]; // Base display order -export const MEMO_FIELD = 'memo'; // Memo field name -export const REWARD_ADDRESSES_FIELD = 'rewardAddresses'; // Reward addresses field -export const SOURCE_CHAIN_FIELD = 'sourceChain'; // Source chain field -export const DESTINATION_CHAIN_FIELD = 'destinationChain'; // Destination chain field - -// Asset and network constants -export const FLR_ASSET_ID = 'FLR'; // Default FLR asset ID - -// Placeholder constants for development -export const FLARE_TX_HEX_PLACEHOLDER = 'flare-tx-hex-placeholder'; // Transaction hex placeholder -export const FLARE_SIGNABLE_PAYLOAD = 'flare-signable-payload'; // Signable payload placeholder -export const FLARE_TRANSACTION_ID_PLACEHOLDER = 'flare-transaction-id-placeholder'; // Transaction ID placeholder -export const PLACEHOLDER_NODE_ID = 'placeholder-node-id'; // Node ID placeholder - -// Chain identifiers (short forms) -export const P_CHAIN_SHORT = 'P'; // P-chain short name -export const X_CHAIN_SHORT = 'X'; // X-chain short name - -// Valid source chains for imports -export const VALID_IMPORT_SOURCE_CHAINS = [P_CHAIN_SHORT, P_CHAIN_FULL, X_CHAIN_SHORT, X_CHAIN_FULL]; // Valid source chains for C-chain imports - -// Valid P-chain import types -export const VALID_P_CHAIN_IMPORT_TYPES = [PLATFORM_VM_IMPORT_TX, IMPORT_TX_TYPE, IMPORT_TYPE, P_CHAIN_IMPORT_TYPE]; // Valid P-chain import types - -// Error messages for transactionBuilder -export const ERROR_NETWORK_ID_MISMATCH = 'Network ID mismatch'; // Network ID validation error -export const ERROR_BLOCKCHAIN_ID_MISMATCH_BUILDER = 'Blockchain ID mismatch'; // Blockchain ID validation error -export const ERROR_INVALID_THRESHOLD = 'Invalid transaction: threshold must be set to 2'; // Threshold validation error -export const ERROR_INVALID_LOCKTIME = 'Invalid transaction: locktime must be 0 or higher'; // Locktime validation error -export const ERROR_UTXOS_EMPTY_ARRAY = "Utxos can't be empty array"; // Empty UTXOS array error -export const ERROR_UTXOS_MISSING_FIELD = 'Utxos required'; // Missing UTXO field error -export const ERROR_FROM_ADDRESSES_REQUIRED = 'from addresses are required'; // Missing from addresses error -export const ERROR_UTXOS_REQUIRED_BUILDER = 'utxos are required'; // Missing UTXOs error -export const ERROR_PARSE_RAW_TRANSACTION = 'Failed to parse raw transaction'; // Raw transaction parsing error -export const ERROR_UNKNOWN_PARSING = 'Unknown error'; // Unknown parsing error - -// UTXO field validation -export const UTXO_REQUIRED_FIELDS = ['outputID', 'amount', 'txid', 'outputidx']; // Required UTXO fields diff --git a/modules/sdk-coin-flrp/src/lib/delegatorTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/delegatorTxBuilder.ts deleted file mode 100644 index c48ad11825..0000000000 --- a/modules/sdk-coin-flrp/src/lib/delegatorTxBuilder.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; -import { Tx } from './iface'; -import { RawTransactionData, StakingExtendedTransaction, DelegatorRawTransactionData } from './types'; -import { - DELEGATOR_TRANSACTION_TYPE, - PRIMARY_DELEGATOR_TYPE, - DELEGATOR_STAKE_TYPE, - SECP256K1_CREDENTIAL_TYPE, - STAKE_OUTPUT_TYPE, - CREDENTIAL_VERSION, - EMPTY_STRING, - ZERO_BIGINT, - ZERO_NUMBER, - DEFAULT_THRESHOLD, - DEFAULT_LOCKTIME, - MEMO_BUFFER_SIZE, - FIRST_ADDRESS_INDEX, -} from './constants'; - -export class DelegatorTxBuilder extends AtomicTransactionBuilder { - protected _nodeID: string; - protected _startTime: bigint; - protected _endTime: bigint; - protected _stakeAmount: bigint; - - /** - * @param coinConfig - */ - constructor(coinConfig: Readonly) { - super(coinConfig); - this._nodeID = EMPTY_STRING; - this._startTime = ZERO_BIGINT; - this._endTime = ZERO_BIGINT; - this._stakeAmount = ZERO_BIGINT; - } - - /** - * get transaction type - * @protected - */ - protected get transactionType(): TransactionType { - return TransactionType.AddDelegator; - } - - /** - * Set the node ID for delegation - * @param nodeID - The node ID to delegate to - */ - nodeID(nodeID: string): this { - if (!nodeID || nodeID.length === ZERO_NUMBER) { - throw new BuildTransactionError('Node ID cannot be empty'); - } - this._nodeID = nodeID; - return this; - } - - /** - * Set the start time for delegation - * @param startTime - Unix timestamp for when delegation starts - */ - startTime(startTime: string | number | bigint): this { - const time = BigInt(startTime); - - // Validate that start time is positive - if (time <= ZERO_NUMBER) { - throw new BuildTransactionError('Start time must be positive'); - } - - // Validate that start time is before end time (if end time is already set) - if (this._endTime > ZERO_NUMBER && time >= this._endTime) { - throw new BuildTransactionError('Start time must be before end time'); - } - - this._startTime = time; - return this; - } - - /** - * Set the end time for delegation - * @param endTime - Unix timestamp for when delegation ends - */ - endTime(endTime: string | number | bigint): this { - const time = BigInt(endTime); - - // Validate that end time is positive - if (time <= ZERO_NUMBER) { - throw new BuildTransactionError('End time must be positive'); - } - - // Validate that end time is after start time (if start time is already set) - if (this._startTime > ZERO_NUMBER && time <= this._startTime) { - throw new BuildTransactionError('End time must be after start time'); - } - - this._endTime = time; - return this; - } - - /** - * Set the stake amount for delegation - * @param amount - Amount to stake (in nFLR) - */ - stakeAmount(amount: string | number | bigint): this { - const stake = BigInt(amount); - if (stake <= ZERO_NUMBER) { - throw new BuildTransactionError('Stake amount must be positive'); - } - this._stakeAmount = stake; - return this; - } - - /** - * Set reward addresses where delegation rewards should be sent - * @param addresses - Array of reward addresses - */ - rewardAddresses(addresses: string[]): this { - if (!addresses || addresses.length === ZERO_NUMBER) { - throw new BuildTransactionError('At least one reward address is required'); - } - // Store reward addresses in the transaction (we'll need to extend the type) - (this.transaction as unknown as StakingExtendedTransaction)._rewardAddresses = addresses; - return this; - } - - /** @inheritdoc */ - initBuilder(tx: Tx): this { - super.initBuilder(tx); - - // Extract delegator-specific fields from transaction - const txData = tx as unknown as RawTransactionData; - const delegatorData = txData as DelegatorRawTransactionData; - - if (delegatorData.nodeID) { - this._nodeID = delegatorData.nodeID; - } - if (delegatorData.startTime) { - this._startTime = BigInt(delegatorData.startTime); - } - if (delegatorData.endTime) { - this._endTime = BigInt(delegatorData.endTime); - } - if (delegatorData.stakeAmount) { - this._stakeAmount = BigInt(delegatorData.stakeAmount); - } - if (delegatorData.rewardAddresses) { - (this.transaction as unknown as StakingExtendedTransaction)._rewardAddresses = delegatorData.rewardAddresses; - } - - return this; - } - - /** - * Verify if the transaction is a delegator transaction - * @param tx - */ - static verifyTxType(tx: unknown): boolean { - // Check if transaction has delegator-specific properties - const delegatorData = tx as DelegatorRawTransactionData; - return !!(delegatorData && delegatorData.nodeID && delegatorData.stakeAmount); - } - - verifyTxType(tx: unknown): boolean { - return DelegatorTxBuilder.verifyTxType(tx); - } - - /** - * Build the delegator transaction using FlareJS PVM API - * @protected - */ - protected async buildFlareTransaction(): Promise { - // Basic validation - if (!this._nodeID) { - throw new BuildTransactionError('Node ID is required for delegator transaction'); - } - if (!this._startTime) { - throw new BuildTransactionError('Start time is required for delegator transaction'); - } - if (!this._endTime) { - throw new BuildTransactionError('End time is required for delegator transaction'); - } - if (!this._stakeAmount) { - throw new BuildTransactionError('Stake amount is required for delegator transaction'); - } - - const rewardAddresses = (this.transaction as unknown as StakingExtendedTransaction)._rewardAddresses; - if (!rewardAddresses || rewardAddresses.length === ZERO_NUMBER) { - throw new BuildTransactionError('Reward addresses are required for delegator transaction'); - } - - // Validate time range - if (this._endTime <= this._startTime) { - throw new BuildTransactionError('End time must be after start time'); - } - - try { - // FlareJS PVM API implementation for delegator transactions - // This creates a structured delegator transaction with proper credential handling - - const enhancedDelegatorTx = { - type: DELEGATOR_TRANSACTION_TYPE, - networkID: this.transaction._networkID, - blockchainID: this.transaction._blockchainID, - - // Enhanced delegator information structure - delegator: { - nodeID: this._nodeID, - startTime: this._startTime, - endTime: this._endTime, - stakeAmount: this._stakeAmount, - rewardAddress: rewardAddresses[FIRST_ADDRESS_INDEX], - // FlareJS delegator markers - _delegatorType: PRIMARY_DELEGATOR_TYPE, - _flareJSReady: true, - _pvmCompatible: true, - }, - - // Enhanced stake information with credentials - stake: { - assetID: this.getAssetId(), - amount: this._stakeAmount, - addresses: this.transaction._fromAddresses, - threshold: this.transaction._threshold || DEFAULT_THRESHOLD, - locktime: this.transaction._locktime || DEFAULT_LOCKTIME, - // FlareJS stake markers - _stakeType: DELEGATOR_STAKE_TYPE, - _flareJSReady: true, - }, - - // Enhanced credential structure for delegators - // This provides proper FlareJS-compatible credential management - credentials: this.transaction._fromAddresses.map((address, index) => ({ - signatures: [], // Will be populated by FlareJS signing process - addressIndices: [index], // Index of the signing address - threshold: DEFAULT_THRESHOLD, // Signature threshold for this credential - // FlareJS credential markers - _credentialType: SECP256K1_CREDENTIAL_TYPE, - _delegatorCredential: true, - _addressIndex: index, - _signingAddress: address, - _flareJSReady: true, - _credentialVersion: CREDENTIAL_VERSION, - })), - - // Enhanced outputs for delegator rewards - outputs: [ - { - assetID: this.getAssetId(), - amount: this._stakeAmount, - addresses: [rewardAddresses[FIRST_ADDRESS_INDEX]], - threshold: DEFAULT_THRESHOLD, - locktime: this.transaction._locktime || DEFAULT_LOCKTIME, - // FlareJS output markers - _outputType: STAKE_OUTPUT_TYPE, - _rewardOutput: true, - _flareJSReady: true, - }, - ], - - // Transaction metadata - memo: Buffer.alloc(MEMO_BUFFER_SIZE), - }; - - this.transaction.setTransaction(enhancedDelegatorTx); - } catch (error) { - throw new BuildTransactionError( - `Failed to build delegator transaction: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - } -} diff --git a/modules/sdk-coin-flrp/src/lib/errors.ts b/modules/sdk-coin-flrp/src/lib/errors.ts deleted file mode 100644 index 7627e2a0b9..0000000000 --- a/modules/sdk-coin-flrp/src/lib/errors.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BuildTransactionError } from '@bitgo/sdk-core'; - -export class AddressValidationError extends BuildTransactionError { - constructor(malformedAddress: string) { - super(`The address '${malformedAddress}' is not a well-formed flrp address`); - this.name = AddressValidationError.name; - } -} - -export class InvalidFeeError extends BuildTransactionError { - constructor(type?: string, expectedType?: string) { - super(`The specified type: "${type}" is not valid. Please provide the type: "${expectedType}"`); - this.name = InvalidFeeError.name; - } -} diff --git a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts deleted file mode 100644 index dddb9fe41a..0000000000 --- a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; -import { - ASSET_ID_LENGTH, - OBJECT_TYPE_STRING, - STRING_TYPE, - BIGINT_TYPE, - DESTINATION_CHAIN_PROP, - DESTINATION_CHAIN_ID_PROP, - EXPORTED_OUTPUTS_PROP, - OUTS_PROP, - INPUTS_PROP, - INS_PROP, - NETWORK_ID_PROP, - BLOCKCHAIN_ID_PROP, -} from './constants'; - -// Lightweight interface placeholders replacing Avalanche SDK transaction shapes -interface FlareExportInputShape { - address: string; - amount: bigint; // includes exported amount + fee - assetId: Buffer; - nonce: bigint; -} - -interface FlareExportOutputShape { - addresses: string[]; // destination P-chain addresses - amount: bigint; // exported amount - assetId: Buffer; -} - -interface FlareUnsignedExportTx { - networkId: number; - sourceBlockchainId: Buffer; // C-chain id - destinationBlockchainId: Buffer; // P-chain id - inputs: FlareExportInputShape[]; - outputs: FlareExportOutputShape[]; -} - -interface FlareSignedExportTx { - unsignedTx: FlareUnsignedExportTx; - // credentials placeholder - credentials: unknown[]; -} - -type RawFlareExportTx = FlareSignedExportTx; // for now treat them the same - -export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { - private _amount?: bigint; - private _nonce?: bigint; - - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - /** - * Utxos are not required in Export Tx in C-Chain. - * Override utxos to prevent used by throwing a error. - * - * @param {DecodedUtxoObj[]} value ignored - */ - utxos(_value: unknown[]): this { - throw new BuildTransactionError('utxos are not required in Export Tx in C-Chain'); - } - - /** - * Amount is a long that specifies the quantity of the asset that this output owns. Must be positive. - * The transaction output amount add a fixed fee that will be paid upon import. - * - * @param {BN | string} amount The withdrawal amount - */ - amount(amount: bigint | number | string): this { - const n = (typeof amount === BIGINT_TYPE ? amount : BigInt(amount)) as bigint; - this.validateAmount(n); - this._amount = n; - return this; - } - - /** - * Set the nonce of C-Chain sender address - * - * @param {number | string} nonce - number that can be only used once - */ - nonce(nonce: bigint | number | string): this { - const n = (typeof nonce === BIGINT_TYPE ? nonce : BigInt(nonce)) as bigint; - this.validateNonce(n); - this._nonce = n; - return this; - } - - /** - * Export tx target P wallet. - * - * @param pAddresses - */ - to(pAddresses: string | string[]): this { - const pubKeys = Array.isArray(pAddresses) ? pAddresses : pAddresses.split('~'); - // For now ensure they are stored as bech32 / string addresses directly - this.transaction._to = pubKeys.map((a) => a.toString()); - return this; - } - - protected get transactionType(): TransactionType { - return TransactionType.Export; - } - - initBuilder(raw: RawFlareExportTx): this { - // Basic shape validation - const unsigned = raw.unsignedTx; - if (unsigned.networkId !== this.transaction._networkID) { - throw new Error('Network ID mismatch'); - } - if (!unsigned.sourceBlockchainId.equals(this.transaction._blockchainID)) { - throw new Error('Blockchain ID mismatch'); - } - if (unsigned.outputs.length !== 1) { - throw new BuildTransactionError('Transaction can have one output'); - } - if (unsigned.inputs.length !== 1) { - throw new BuildTransactionError('Transaction can have one input'); - } - const out = unsigned.outputs[0]; - const inp = unsigned.inputs[0]; - if (!out.assetId.equals(this.transaction._assetId) || !inp.assetId.equals(this.transaction._assetId)) { - throw new Error('AssetID mismatch'); - } - this.transaction._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(); - this.transaction._fee.fee = fee.toString(); - this.transaction._fee.size = 1; - this.transaction._fromAddresses = [inp.address]; - this._nonce = inp.nonce; - this.transaction.setTransaction(raw); - return this; - } - - // Verify transaction type for FlareJS export transactions - static verifyTxType(_tx: unknown): _tx is FlareUnsignedExportTx { - if (!_tx) { - return true; // Maintain compatibility with existing tests - } - - try { - // If it's an object, do basic validation - if (typeof _tx === OBJECT_TYPE_STRING) { - const tx = _tx as Record; - - // Basic structure validation for export transactions - const hasDestinationChain = DESTINATION_CHAIN_PROP in tx || DESTINATION_CHAIN_ID_PROP in tx; - const hasExportedOutputs = EXPORTED_OUTPUTS_PROP in tx || OUTS_PROP in tx; - const hasInputs = INPUTS_PROP in tx || INS_PROP in tx; - const hasNetworkID = NETWORK_ID_PROP in tx; - const hasBlockchainID = BLOCKCHAIN_ID_PROP in tx; - - // If it has the expected structure, validate it; otherwise return true for compatibility - if (hasDestinationChain || hasExportedOutputs || hasInputs || hasNetworkID || hasBlockchainID) { - return hasDestinationChain && hasExportedOutputs && hasInputs && hasNetworkID && hasBlockchainID; - } - } - - return true; // Default to true for backward compatibility - } catch { - return true; // Default to true for backward compatibility - } - } - - verifyTxType(_tx: unknown): _tx is FlareUnsignedExportTx { - return ExportInCTxBuilder.verifyTxType(_tx); - } - - /** - * Build the export in C-chain transaction using FlareJS API - * @protected - */ - protected buildFlareTransaction(): void { - if (this.transaction.hasCredentials) { - return; - } - if (this._amount === undefined) { - throw new Error('amount is required'); - } - if (this.transaction._fromAddresses.length !== 1) { - throw new Error('sender is one and required'); - } - if (this.transaction._to.length === 0) { - throw new Error('to is required'); - } - if (!this.transaction._fee.feeRate) { - throw new Error('fee rate is required'); - } - if (this._nonce === undefined) { - throw new Error('nonce is required'); - } - - // Build export transaction using FlareJS API patterns - const feeRate = BigInt(this.transaction._fee.feeRate); - const fixed = this.fixedFee; - const totalFee = feeRate + fixed; - - // Implement FlareJS evm.newExportTx with enhanced structure - const fromAddress = this.transaction._fromAddresses[0]; - const exportAmount = this._amount || BigInt(0); - - const input: FlareExportInputShape = { - address: fromAddress, - amount: exportAmount + totalFee, - assetId: Buffer.alloc(ASSET_ID_LENGTH), - nonce: this._nonce || BigInt(0), - }; - - const output: FlareExportOutputShape = { - addresses: this.transaction._to, - amount: exportAmount, - assetId: Buffer.alloc(ASSET_ID_LENGTH), - }; - - // Create FlareJS-ready unsigned transaction - const unsigned: FlareUnsignedExportTx = { - networkId: this.transaction._networkID, - sourceBlockchainId: this.transaction._blockchainID, - destinationBlockchainId: this._externalChainId || Buffer.alloc(ASSET_ID_LENGTH), - inputs: [input], - outputs: [output], - }; - - // Create signed transaction structure - const signed: FlareSignedExportTx = { unsignedTx: unsigned, credentials: [] }; - - // Update transaction fee information - this.transaction._fee.fee = totalFee.toString(); - this.transaction._fee.size = 1; - - // Set the enhanced transaction - this.transaction.setTransaction(signed); - } - - /** @inheritdoc */ - protected fromImplementation(raw: string | RawFlareExportTx): { _tx?: unknown } { - if (typeof raw === STRING_TYPE) { - // Future: parse hex or serialized form. For now treat as opaque raw tx. - this.transaction.setTransaction(raw); - return this.transaction; - } - return this.initBuilder(raw as RawFlareExportTx).transaction; - } - - /** - * Check the amount is positive. - * @param amount - */ - validateNonce(nonce: bigint): void { - if (nonce < 0n) { - throw new BuildTransactionError('Nonce must be greater or equal than 0'); - } - } -} diff --git a/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts deleted file mode 100644 index 194326b9df..0000000000 --- a/modules/sdk-coin-flrp/src/lib/exportInPTxBuilder.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { TransactionType } from '@bitgo/sdk-core'; -import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; -import { - ASSET_ID_LENGTH, - DEFAULT_BASE_FEE, - ZERO_BIGINT, - FLARE_MAINNET_NETWORK_ID, - FLARE_TESTNET_NETWORK_ID, - MAINNET_TYPE, - EXPORT_TRANSACTION_TYPE, - FIRST_ARRAY_INDEX, - SECP256K1_CREDENTIAL_TYPE, - EMPTY_BUFFER_SIZE, - ERROR_EXPORT_NOT_IMPLEMENTED, - ERROR_DESTINATION_CHAIN_REQUIRED, - ERROR_SOURCE_ADDRESSES_REQUIRED, - ERROR_DESTINATION_ADDRESSES_REQUIRED, - ERROR_EXPORT_AMOUNT_POSITIVE, -} from './constants'; - -export class ExportInPTxBuilder extends AtomicTransactionBuilder { - private _amount = ZERO_BIGINT; - - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.Export; - } - - /** - * Amount is a long that specifies the quantity of the asset that this output owns. Must be positive. - * - * @param {BN | string} amount The withdrawal amount - */ - amount(value: bigint | string | number): this { - const v = typeof value === 'bigint' ? value : BigInt(value); - this.validateAmount(v); - this._amount = v; - return this; - } - - /** @inheritdoc */ - initBuilder(_tx: unknown): this { - super.initBuilder(_tx); - return this; - } - - // Type verification not yet implemented for Flare P-chain - static verifyTxType(_baseTx: unknown): boolean { - return false; - } - - verifyTxType(_baseTx: unknown): boolean { - return ExportInPTxBuilder.verifyTxType(_baseTx); - } - - /** - * Create the internal transaction using FlareJS API patterns. - * @protected - */ - protected buildFlareTransaction(): void { - // P-chain export transaction implementation - // This creates a structured export transaction from P-chain to C-chain - - // For compatibility with existing tests, maintain placeholder behavior initially - // but allow progression when proper setup is provided - - // Check if this is a basic test call (minimal setup) - // This maintains compatibility with existing tests expecting "not implemented" - if (!this._externalChainId && !this.transaction._fromAddresses.length && !this.transaction._to.length) { - // Maintain compatibility with existing tests expecting "not implemented" - throw new Error(ERROR_EXPORT_NOT_IMPLEMENTED); - } - - // Enhanced validation for real usage - if (!this._externalChainId) { - throw new Error(ERROR_DESTINATION_CHAIN_REQUIRED); - } - - if (!this.transaction._fromAddresses.length) { - throw new Error(ERROR_SOURCE_ADDRESSES_REQUIRED); - } - - if (!this.transaction._to.length) { - throw new Error(ERROR_DESTINATION_ADDRESSES_REQUIRED); - } - - if (this._amount <= ZERO_BIGINT) { - throw new Error(ERROR_EXPORT_AMOUNT_POSITIVE); - } - - // Enhanced P-chain export transaction structure compatible with FlareJS pvm.newExportTx - const enhancedExportTx = { - type: EXPORT_TRANSACTION_TYPE, - networkID: this._coinConfig.network.type === MAINNET_TYPE ? FLARE_MAINNET_NETWORK_ID : FLARE_TESTNET_NETWORK_ID, // Flare mainnet: 1, testnet: 5 - blockchainID: this.transaction._blockchainID, - destinationChain: this._externalChainId, - - // Enhanced input structure ready for FlareJS pvm.newExportTx - inputs: this._utxos.map((input) => ({ - txID: Buffer.alloc(ASSET_ID_LENGTH), // Transaction ID from UTXO - outputIndex: FIRST_ARRAY_INDEX, - assetID: this.transaction._assetId, - amount: BigInt(input.amount), - address: input.addresses[FIRST_ARRAY_INDEX], - // FlareJS compatibility markers - _flareJSReady: true, - _pvmCompatible: true, - })), - - // Enhanced output structure for P-chain exports - exportedOutputs: [ - { - assetID: this.transaction._assetId, - amount: this._amount, - addresses: this.transaction._to, - threshold: this.transaction._threshold, - locktime: this.transaction._locktime, - // FlareJS export output markers - _destinationChain: this._externalChainId, - _flareJSReady: true, - }, - ], - - // Enhanced fee structure for P-chain operations - fee: BigInt(this.transaction._fee.fee) || BigInt(DEFAULT_BASE_FEE), // Default P-chain fee - - // Credential placeholders ready for FlareJS integration - credentials: this.transaction._fromAddresses.map(() => ({ - signatures: [], // Will be populated by FlareJS signing - _credentialType: SECP256K1_CREDENTIAL_TYPE, - _flareJSReady: true, - })), - - // Transaction metadata - memo: Buffer.alloc(EMPTY_BUFFER_SIZE), - }; - - // Store the transaction structure - this.transaction.setTransaction(enhancedExportTx); - } - - /** - * Create the ExportedOut where the recipient address are the sender. - * Later a importTx should complete the operations signing with the same keys. - * @protected - */ - protected exportedOutputs(): unknown[] { - return []; - } -} diff --git a/modules/sdk-coin-flrp/src/lib/iface.ts b/modules/sdk-coin-flrp/src/lib/iface.ts index b94370295a..6a4c42c492 100644 --- a/modules/sdk-coin-flrp/src/lib/iface.ts +++ b/modules/sdk-coin-flrp/src/lib/iface.ts @@ -1,15 +1,20 @@ -import { - TransactionExplanation as BaseTransactionExplanation, - Entry, - TransactionType, - SignTransactionOptions, - VerifyTransactionOptions, - TransactionParams, -} from '@bitgo/sdk-core'; -import { UnsignedTx, TransferableOutput, avaxSerial } from '@flarenetwork/flarejs'; +import { TransactionExplanation as BaseTransactionExplanation, Entry, TransactionType } from '@bitgo/sdk-core'; +import { pvmSerial, UnsignedTx, TransferableOutput, evmSerial } from '@flarenetwork/flarejs'; + +/** + * Enum for Flare transaction types + */ +export enum FlareTransactionType { + EvmExportTx = 'evm.ExportTx', + EvmImportTx = 'evm.ImportTx', + PvmExportTx = 'pvm.ExportTx', + PvmImportTx = 'pvm.ImportTx', +} + export interface FlrpEntry extends Entry { id: string; } + export interface TransactionExplanation extends BaseTransactionExplanation { type: TransactionType; rewardAddresses: string[]; @@ -17,11 +22,11 @@ export interface TransactionExplanation extends BaseTransactionExplanation { } /** - * Method names for the transaction method. Names change based on the type of transaction e.g 'bond' for the staking transaction + * Method names for the transaction method. Names change based on the type of transaction */ export enum MethodNames { - addDelegator, - addValidator, + addPermissionlessValidator, + addPermissionlessDelegator, } /** @@ -39,7 +44,6 @@ export interface TxData { changeOutputs: Entry[]; sourceChain?: string; destinationChain?: string; - memo?: string; // Memo field for transaction metadata } /** @@ -52,6 +56,7 @@ export interface TxData { */ export type DecodedUtxoObj = { outputID: number; + locktime?: string; amount: string; txid: string; outputidx: string; @@ -61,80 +66,72 @@ export type DecodedUtxoObj = { }; /** - * FlareJS uses string-based TypeSymbols instead of numeric type IDs - * For SECP256K1 Transfer Output, use TypeSymbols.TransferOutput from @flarenetwork/flarejs + * TypeId value for SECP256K1 Transfer Output * - * @see https://docs.flare.network/ for Flare network documentation - * @deprecated Use TypeSymbols.TransferOutput from @flarenetwork/flarejs instead + * Similar to Avalanche P-Chain's SECP256K1 Transfer Output */ -export const SECP256K1_Transfer_Output = 7; // Legacy - FlareJS uses TypeSymbols.TransferOutput +export const SECP256K1_Transfer_Output = 7; + +/** + * TypeId value for Stakeable Lock Output + */ +export const SECP256K1_STAKEABLE_LOCK_OUT = 22; export const ADDRESS_SEPARATOR = '~'; export const INPUT_SEPARATOR = ':'; -// FlareJS 1.3.2 type definitions - using avm and platformvm modules -export type DeprecatedTx = unknown; // Placeholder for backward compatibility -export type DeprecatedBaseTx = unknown; // Placeholder for backward compatibility -export type Tx = UnsignedTx; // FlareJS UnsignedTx (unified type in 4.0.5) -export type BaseTx = avaxSerial.BaseTx; // FlareJS BaseTx -export type AvaxTx = avaxSerial.AvaxTx; // FlareJS AvaxTx -export type DeprecatedOutput = unknown; // Placeholder for backward compatibility -export type Output = TransferableOutput; // FlareJS TransferableOutput (unified type in 4.0.5) - -// FLRP-specific interfaces extending SDK-core interfaces -export interface FlrpSignTransactionOptions extends SignTransactionOptions { - txPrebuild: { - txHex: string; - }; - prv: string; -} - -export interface FlrpVerifyTransactionOptions extends VerifyTransactionOptions { - txParams: FlrpTransactionParams; -} - +// Simplified type definitions for Flare +export type Tx = + | pvmSerial.BaseTx + | UnsignedTx + | evmSerial.ExportTx + | evmSerial.ImportTx + | pvmSerial.ExportTx + | pvmSerial.ImportTx; +export type BaseTx = pvmSerial.BaseTx; +export type Output = TransferableOutput; +export type DeprecatedTx = unknown; +/** + * Interface for staking options + */ export interface FlrpTransactionStakingOptions { nodeID: string; + startTime: string; + endTime: string; amount: string; - startTime?: string; - endTime?: string; - delegationFeeRate?: number; + rewardAddress?: string; + delegationFee?: number; } -export interface FlrpTransactionParams extends TransactionParams { - type: string; - recipients?: Array<{ +/** + * Interface for transaction parameters + */ +export interface FlrpTransactionParams { + recipients?: { address: string; amount: string; - }>; + }[]; stakingOptions?: FlrpTransactionStakingOptions; unspents?: string[]; -} - -export interface ExplainTransactionOptions { - txHex?: string; - halfSigned?: { - txHex: string; - }; + type?: string; } /** - * Memo utility interfaces for FlareJS integration + * Interface for transaction verification options */ -export interface MemoData { - text?: string; // UTF-8 string memo - bytes?: Uint8Array; // Raw byte array memo - json?: Record; // JSON object memo (will be stringified) +export interface FlrpVerifyTransactionOptions { + txPrebuild: { + txHex: string; + }; + txParams: FlrpTransactionParams; } /** - * SpendOptions interface matching FlareJS patterns - * Based on FlareJS SpendOptions with memo support + * Interface for explaining transaction options */ -export interface FlrpSpendOptions { - minIssuanceTime?: bigint; - changeAddresses?: string[]; // BitGo uses string addresses, FlareJS uses Uint8Array[] - threshold?: number; - memo?: Uint8Array; // FlareJS memo format (byte array) - locktime?: bigint; +export interface ExplainTransactionOptions { + txHex?: string; + halfSigned?: { + txHex: string; + }; } diff --git a/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts deleted file mode 100644 index 6688a42423..0000000000 --- a/modules/sdk-coin-flrp/src/lib/importInCTxBuilder.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; -import { TransferableInput, Credential } from '@flarenetwork/flarejs'; -import { Buffer } from 'buffer'; -import utils, { createHexRegex } from './utils'; -import { Tx, DecodedUtxoObj } from './iface'; -import BigNumber from 'bignumber.js'; -import { BaseExtendedTransaction } from './types'; -import { - ASSET_ID_LENGTH, - OUTPUT_INDEX_HEX_LENGTH, - CHAIN_ID_HEX_LENGTH, - DEFAULT_BASE_FEE, - DEFAULT_EVM_GAS_FEE, - INPUT_FEE, - OUTPUT_FEE, - MINIMUM_FEE, - EMPTY_STRING, - AMOUNT_STRING_ZERO, - FIRST_ARRAY_INDEX, - DEFAULT_THRESHOLD, - DEFAULT_LOCKTIME, - ZERO_NUMBER, - ERROR_TRANSACTION_REQUIRED, - ERROR_BLOCKCHAIN_ID_MISMATCH, - ERROR_TRANSACTION_PARSE_FAILED, - ERROR_FAILED_INITIALIZE_BUILDER, - OBJECT_TYPE_STRING, - HEX_ENCODING, - VALID_IMPORT_SOURCE_CHAINS, - P_CHAIN_SHORT, - UTF8_ENCODING, - IMPORT_C_TYPE, - TRANSFERABLE_INPUT_TYPE, - CREDENTIAL_TYPE, -} from './constants'; - -/** - * Flare P->C Import Transaction Builder - * Builds import transactions from P-chain to C-chain using FlareJS - */ -export class ImportInCTxBuilder extends AtomicInCTransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - /** - * C-chain address who is target of the import. - * Address format is Ethereum-like for Flare C-chain - * @param {string} cAddress - C-chain address (hex format) - */ - to(cAddress: string): this { - // Validate and normalize C-chain address - if (!utils.isValidAddress(cAddress)) { - throw new BuildTransactionError(`Invalid C-chain address: ${cAddress}`); - } - this.transaction._to = [cAddress]; - return this; - } - - protected get transactionType(): TransactionType { - return TransactionType.Import; - } - - /** @inheritdoc */ - initBuilder(tx: Tx): this { - if (!tx) { - throw new BuildTransactionError(ERROR_TRANSACTION_REQUIRED); - } - - // Handle both UnsignedTx and signed transaction formats - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsignedTx = (tx as any).unsignedTx || tx; - - try { - // Extract network and blockchain validation - if (unsignedTx.networkID !== undefined && unsignedTx.networkID !== this.transaction._networkID) { - throw new BuildTransactionError( - `Network ID mismatch: expected ${this.transaction._networkID}, got ${unsignedTx.networkID}` - ); - } - - if (unsignedTx.blockchainID && !unsignedTx.blockchainID.equals(this.transaction._blockchainID)) { - throw new BuildTransactionError(ERROR_BLOCKCHAIN_ID_MISMATCH); - } - - // Extract C-chain import transaction details - if (unsignedTx.importIns && Array.isArray(unsignedTx.importIns)) { - // Extract UTXOs from import inputs (typically from P-chain) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const utxos: DecodedUtxoObj[] = unsignedTx.importIns.map((importIn: any) => ({ - id: importIn.txID?.toString() || EMPTY_STRING, - outputIndex: importIn.outputIndex || FIRST_ARRAY_INDEX, - amount: importIn.input?.amount?.toString() || AMOUNT_STRING_ZERO, - assetId: importIn.input?.assetID || Buffer.alloc(ASSET_ID_LENGTH), - address: importIn.input?.addresses?.[FIRST_ARRAY_INDEX] || EMPTY_STRING, - threshold: importIn.input?.threshold || DEFAULT_THRESHOLD, - locktime: importIn.input?.locktime || DEFAULT_LOCKTIME, - })); - this.addUtxos(utxos); - } - - // Extract outputs (C-chain destination) - if (unsignedTx.outs && Array.isArray(unsignedTx.outs)) { - const outputs = unsignedTx.outs; - if (outputs.length > ZERO_NUMBER) { - const firstOutput = outputs[FIRST_ARRAY_INDEX]; - - // C-chain uses Ethereum-style addresses - if (firstOutput.addresses && Array.isArray(firstOutput.addresses)) { - // Set the first address as the destination - if (firstOutput.addresses.length > ZERO_NUMBER) { - this.to(firstOutput.addresses[FIRST_ARRAY_INDEX]); - } - } - - // Extract amount if present - if (firstOutput.amount) { - // Store output amount for validation - (this.transaction as BaseExtendedTransaction)._outputAmount = firstOutput.amount.toString(); - } - } - } - - // Extract source chain (typically P-chain for C-chain imports) - if (unsignedTx.sourceChain) { - this._externalChainId = Buffer.isBuffer(unsignedTx.sourceChain) - ? unsignedTx.sourceChain - : Buffer.from(unsignedTx.sourceChain, HEX_ENCODING); - } - - // Extract fee information - if (unsignedTx.fee !== undefined) { - this.transaction._fee.fee = unsignedTx.fee.toString(); - } - - // Extract memo if present - if (unsignedTx.memo && unsignedTx.memo.length > 0) { - // Store memo data for later use - (this.transaction as BaseExtendedTransaction)._memo = unsignedTx.memo; - } - - // Set the transaction - this.transaction.setTransaction(tx); - - // Validate transaction type - if (!this.verifyTxType(tx)) { - throw new BuildTransactionError(ERROR_TRANSACTION_PARSE_FAILED); - } - } catch (error) { - if (error instanceof BuildTransactionError) { - throw error; - } - throw new BuildTransactionError(`${ERROR_FAILED_INITIALIZE_BUILDER}: ${error}`); - } - - return this; - } - - /** - * Verify transaction type for FlareJS import transactions - * @param {unknown} unsignedTx - FlareJS UnsignedTx - * @returns {boolean} - Whether transaction is valid import type - */ - static verifyTxType(unsignedTx: unknown): boolean { - try { - // Check if transaction has the structure of an import transaction - const tx = unsignedTx as { - importIns?: unknown[]; - sourceChain?: Buffer | string; - to?: Buffer | string; - type?: string; - }; - - // If transaction is null/undefined, return false - if (!tx || typeof tx !== OBJECT_TYPE_STRING) { - return false; - } - - // If it's a placeholder with type 'import', accept it (for testing) - if (tx.type === 'import') { - return true; - } - - // Check for import transaction specific properties - // ImportTx should have sourceChain, importIns, and destination C-chain address - const hasImportIns = Boolean(tx.importIns && Array.isArray(tx.importIns)); - const hasSourceChain = Boolean( - tx.sourceChain && (Buffer.isBuffer(tx.sourceChain) || typeof tx.sourceChain === 'string') - ); - - // For C-chain imports, check EVM-specific properties - const hasToAddress = Boolean(tx.to && (Buffer.isBuffer(tx.to) || typeof tx.to === 'string')); - - // If it has all import transaction properties, it's valid - if (hasImportIns && hasSourceChain && hasToAddress) { - return true; - } - - // For other cases (empty objects, different types), return false - return false; - } catch (error) { - return false; - } - } - - verifyTxType(unsignedTx: unknown): boolean { - return ImportInCTxBuilder.verifyTxType(unsignedTx); - } - - /** - * Build the import C-chain transaction using FlareJS evm.newImportTx - * @protected - */ - protected buildFlareTransaction(): void { - // if tx has credentials, tx shouldn't change - if (this.transaction.hasCredentials) return; - - if (this.transaction._to.length !== 1) { - throw new BuildTransactionError('C-chain destination address is required'); - } - - if (this._utxos.length === 0) { - throw new BuildTransactionError('UTXOs are required for import transaction'); - } - - try { - // Prepare parameters for FlareJS evm.newImportTx - const toAddress = new Uint8Array(Buffer.from(this.transaction._to[0].replace('0x', ''), 'hex')); - - // Convert our UTXOs to FlareJS format - const flareUtxos = this._utxos.map((utxo) => ({ - txID: utxo.txid, - outputIndex: parseInt(utxo.outputidx, 10), - output: { - amount: () => BigInt(utxo.amount), - assetID: Buffer.alloc(ASSET_ID_LENGTH), // Default asset ID, should be extracted from UTXO in real implementation - addresses: utxo.addresses, - threshold: utxo.threshold, - locktime: 0n, // Default locktime, should be extracted from UTXO in real implementation - }, - })); - - // Get source chain ID (typically P-chain for C-chain imports) - const sourceChainId = this._externalChainId ? this._externalChainId.toString(HEX_ENCODING) : P_CHAIN_SHORT; - - // Calculate fee - const fee = BigInt(this.transaction._fee.fee || DEFAULT_EVM_GAS_FEE); // EVM-style gas fee - - // Prepare source addresses from UTXOs for FlareJS - const fromAddresses = Array.from(new Set(this._utxos.flatMap((utxo) => utxo.addresses))).map((addr) => - Buffer.from(addr, 'hex') - ); - - // Enhanced implementation - prepare for FlareJS integration - // Create transaction structure compatible with FlareJS evm.newImportTx - const enhancedTx = { - networkID: this.transaction._networkID, - blockchainID: this.transaction._blockchainID, - sourceChain: sourceChainId, - importedInputs: flareUtxos.map((utxo) => ({ - ...utxo, - // Add FlareJS-compatible fields - utxoID: Buffer.from( - utxo.txID + utxo.outputIndex.toString(16).padStart(OUTPUT_INDEX_HEX_LENGTH, AMOUNT_STRING_ZERO), - HEX_ENCODING - ), - assetID: utxo.output.assetID, - amount: utxo.output.amount(), - })), - outputs: [ - { - address: toAddress, - amount: this.calculateTotalAmount(flareUtxos) - fee, - assetID: Buffer.alloc(ASSET_ID_LENGTH), // Default asset ID for Flare native token - }, - ], - fee, - type: IMPORT_C_TYPE, - fromAddresses: fromAddresses.map((addr) => addr.toString('hex')), - toAddress: Buffer.from(toAddress).toString('hex'), - // Add FlareJS-specific metadata for future integration - _flareJSReady: true, - }; - - this.transaction.setTransaction(enhancedTx); - } catch (error) { - throw new BuildTransactionError(`Failed to build import transaction: ${error}`); - } - } - - /** - * Calculate total amount from UTXOs - * @private - */ - private calculateTotalAmount(utxos: Array<{ output: { amount: () => bigint } }>): bigint { - return utxos.reduce((total, utxo) => { - const amount = typeof utxo.output.amount === 'function' ? utxo.output.amount() : BigInt(utxo.output.amount || 0); - return total + amount; - }, 0n); - } - - /** - * Create inputs for the import transaction from UTXOs - * @returns {Object} - Inputs, total amount, and credentials - * @protected - */ - protected createInputs(): { - inputs: TransferableInput[]; - credentials: Credential[]; - amount: BigNumber; - } { - if (this._utxos.length === 0) { - throw new BuildTransactionError('No UTXOs available for import'); - } - - const inputs: TransferableInput[] = []; - const credentials: Credential[] = []; - let totalAmount = new BigNumber(0); - - // Process each UTXO to create inputs - this._utxos.forEach((utxo: DecodedUtxoObj) => { - // Convert UTXO to FlareJS-compatible TransferableInput format - const amount = new BigNumber(utxo.amount); - totalAmount = totalAmount.plus(amount); - - // Create enhanced input structure ready for FlareJS integration - const enhancedInput = { - // UTXO identification - txID: Buffer.from(utxo.txid, 'hex'), - outputIndex: parseInt(utxo.outputidx, 10), - - // Asset information - assetID: Buffer.alloc(ASSET_ID_LENGTH), // Should be extracted from UTXO in real implementation - - // Transfer details - amount: amount.toString(), - locktime: BigInt(0), - threshold: utxo.threshold, - addresses: utxo.addresses.map((addr) => Buffer.from(addr, 'hex')), - - // FlareJS compatibility markers - _flareJSReady: true, - _type: TRANSFERABLE_INPUT_TYPE, - - // Methods for FlareJS compatibility - getAmount: () => BigInt(amount.toString()), - getAssetID: () => Buffer.alloc(ASSET_ID_LENGTH), - getUTXOID: () => - Buffer.from(utxo.txid + utxo.outputidx.padStart(OUTPUT_INDEX_HEX_LENGTH, AMOUNT_STRING_ZERO), HEX_ENCODING), - }; - - inputs.push(enhancedInput as unknown as TransferableInput); - - // Create enhanced credential structure ready for FlareJS integration - const enhancedCredential = { - // Signature management - signatureIndices: Array.from({ length: utxo.threshold }, (_, i) => i), - signatures: [] as Buffer[], // Will be populated during signing - - // FlareJS compatibility markers - _flareJSReady: true, - _type: CREDENTIAL_TYPE, - - // Methods for FlareJS compatibility - addSignature: (signature: Buffer) => enhancedCredential.signatures.push(signature), - getSignatureIndices: () => enhancedCredential.signatureIndices, - serialize: () => Buffer.alloc(0), - }; - - credentials.push(enhancedCredential as unknown as Credential); - }); - - return { - inputs, - credentials, - amount: totalAmount, - }; - } - - /** - * Calculate import transaction fee using FlareJS - * @param {TransferableInput[]} inputs - Transaction inputs - * @returns {BigNumber} - Calculated fee amount - * @protected - */ - protected calculateImportFee(inputs: TransferableInput[]): BigNumber { - // Implement FlareJS-compatible fee calculation - // This follows FlareJS fee calculation patterns for C-chain imports - - const baseFee = new BigNumber(this.transaction._fee.feeRate || DEFAULT_BASE_FEE); // 1M nanoFLR default - const inputCount = inputs.length; - const outputCount = 1; // Single C-chain output - - // FlareJS-style fee calculation for import transactions - // Base fee covers transaction overhead - // Input fees cover UTXO processing - // Output fees cover result generation - - const inputFee = new BigNumber(INPUT_FEE); // 100K nanoFLR per input (FlareJS standard) - const outputFee = new BigNumber(OUTPUT_FEE); // 50K nanoFLR per output (FlareJS standard) - - // Calculate total fee: base + inputs + outputs - const totalFee = baseFee - .plus(new BigNumber(inputCount).times(inputFee)) - .plus(new BigNumber(outputCount).times(outputFee)); - - // Add C-chain specific fees (EVM gas consideration) - const evmGasFee = new BigNumber(DEFAULT_EVM_GAS_FEE); // Standard EVM transfer gas - const finalFee = totalFee.plus(evmGasFee); - - // Ensure minimum fee threshold - const minimumFee = new BigNumber(MINIMUM_FEE); // 1M nanoFLR minimum - return BigNumber.max(finalFee, minimumFee); - } - - /** - * Add UTXOs to be used as inputs for the import transaction - * @param {DecodedUtxoObj[]} utxos - UTXOs from P-chain to import - */ - addUtxos(utxos: DecodedUtxoObj[]): this { - if (!Array.isArray(utxos)) { - throw new BuildTransactionError('UTXOs must be an array'); - } - - this._utxos = [...this._utxos, ...utxos]; - return this; - } - - /** - * Set the source chain for the import (typically P-chain) - * @param {string} chainId - Source chain ID - */ - sourceChain(chainId: string): this { - // Validate and set source chain ID for C-chain imports - if (!chainId || typeof chainId !== 'string') { - throw new BuildTransactionError('Source chain ID must be a non-empty string'); - } - - // Valid source chains for C-chain imports in Flare network - const validSourceChains = VALID_IMPORT_SOURCE_CHAINS; - const chainIdNormalized = chainId.replace('-chain', '').toUpperCase(); - - // Check if it's a predefined chain identifier - if (validSourceChains.some((chain) => chain.replace('-chain', '').toUpperCase() === chainIdNormalized)) { - // Store normalized chain ID (e.g., 'P' for P-chain) - this._externalChainId = Buffer.from(chainIdNormalized, UTF8_ENCODING); - return this; - } - - // Check if it's a hex-encoded chain ID (CHAIN_ID_HEX_LENGTH characters for FlareJS) - if (createHexRegex(CHAIN_ID_HEX_LENGTH).test(chainId)) { - this._externalChainId = Buffer.from(chainId, 'hex'); - return this; - } - - // Check if it's a CB58-encoded chain ID (FlareJS format) - if (utils.isValidAddress(chainId)) { - this._externalChainId = utils.cb58Decode(chainId); - return this; - } - - // If none of the above, try to decode as hex or use as-is - try { - this._externalChainId = Buffer.from(chainId, 'hex'); - } catch (error) { - this._externalChainId = Buffer.from(chainId, UTF8_ENCODING); - } - - return this; - } -} diff --git a/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts deleted file mode 100644 index fb0169fa62..0000000000 --- a/modules/sdk-coin-flrp/src/lib/importInPTxBuilder.ts +++ /dev/null @@ -1,519 +0,0 @@ -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { pvm, utils as flareUtils, TransferableInput, TransferableOutput, Credential } from '@flarenetwork/flarejs'; -import { Buffer } from 'buffer'; -import utils, { createFlexibleHexRegex } from './utils'; -import { Tx, DecodedUtxoObj } from './iface'; -import BigNumber from 'bignumber.js'; -import { BaseExtendedTransaction } from './types'; -import { - ASSET_ID_LENGTH, - DEFAULT_BASE_FEE, - SECP256K1_SIGNATURE_LENGTH, - MAX_CHAIN_ID_LENGTH, - C_CHAIN, - HEX_ENCODING, - OBJECT_TYPE_STRING, - STRING_TYPE, - NUMBER_TYPE, - VALID_P_CHAIN_IMPORT_TYPES, - EXPORT_TYPE, - SEND_TYPE, - IMPORT_TYPE, - P_CHAIN_FULL, - C_CHAIN_FULL, - UTF8_ENCODING, -} from './constants'; - -/** - * Flare P-chain Import Transaction Builder - * Builds import transactions within P-chain (typically from C-chain to P-chain) using FlareJS - */ -export class ImportInPTxBuilder extends AtomicTransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - // Set external chain ID to C-chain for P-chain imports - const network = this.transaction._network as { cChainBlockchainID?: string }; - if (network.cChainBlockchainID) { - this._externalChainId = utils.cb58Decode - ? utils.cb58Decode(network.cChainBlockchainID) - : Buffer.from(network.cChainBlockchainID); - } - } - - protected get transactionType(): TransactionType { - return TransactionType.Import; - } - - /** - * Initialize builder from existing FlareJS P-chain import transaction - * @param {Tx} tx - FlareJS UnsignedTx or signed transaction to initialize from - */ - initBuilder(tx: Tx): this { - if (!tx) { - throw new BuildTransactionError('Transaction is required for initialization'); - } - - // Handle both UnsignedTx and signed transaction formats - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const unsignedTx = (tx as any).unsignedTx || tx; - - try { - // Extract network and blockchain validation - if (unsignedTx.networkID !== undefined && unsignedTx.networkID !== this.transaction._networkID) { - throw new BuildTransactionError( - `Network ID mismatch: expected ${this.transaction._networkID}, got ${unsignedTx.networkID}` - ); - } - - if (unsignedTx.blockchainID && !unsignedTx.blockchainID.equals(this.transaction._blockchainID)) { - throw new BuildTransactionError('Blockchain ID mismatch'); - } - - // Extract P-chain import transaction details - if (unsignedTx.importIns && Array.isArray(unsignedTx.importIns)) { - // Extract UTXOs from import inputs - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const utxos: DecodedUtxoObj[] = unsignedTx.importIns.map((importIn: any) => ({ - id: importIn.txID?.toString() || '', - outputIndex: importIn.outputIndex || 0, - amount: importIn.input?.amount?.toString() || '0', - assetId: importIn.input?.assetID || Buffer.alloc(ASSET_ID_LENGTH), - address: importIn.input?.addresses?.[0] || '', - threshold: importIn.input?.threshold || 1, - locktime: importIn.input?.locktime || 0n, - })); - this.addUtxos(utxos); - } - - // Extract outputs (P-chain destination) - if (unsignedTx.outs && Array.isArray(unsignedTx.outs)) { - const outputs = unsignedTx.outs; - if (outputs.length > 0) { - const firstOutput = outputs[0]; - - // Set locktime and threshold from first output - if (firstOutput.locktime !== undefined) { - this.locktime(firstOutput.locktime); - } - - if (firstOutput.threshold !== undefined) { - this.threshold(firstOutput.threshold); - } - - // Extract addresses - if (firstOutput.addresses && Array.isArray(firstOutput.addresses)) { - this.transaction._to = firstOutput.addresses; - } - } - } - - // Extract source chain (typically C-chain for P-chain imports) - if (unsignedTx.sourceChain) { - this._externalChainId = Buffer.isBuffer(unsignedTx.sourceChain) - ? unsignedTx.sourceChain - : Buffer.from(unsignedTx.sourceChain, HEX_ENCODING); - } - - // Extract memo if present - if (unsignedTx.memo && unsignedTx.memo.length > 0) { - // Store memo data for later use - (this.transaction as BaseExtendedTransaction)._memo = unsignedTx.memo; - } - - // Set the transaction - this.transaction.setTransaction(tx); - - // Validate transaction type - if (!this.verifyTxType(tx)) { - throw new BuildTransactionError('Transaction cannot be parsed or has an unsupported transaction type'); - } - } catch (error) { - if (error instanceof BuildTransactionError) { - throw error; - } - throw new BuildTransactionError(`Failed to initialize builder from transaction: ${error}`); - } - - return this; - } - - /** - * Verify transaction type for FlareJS P-chain import transactions - * @param {unknown} unsignedTx - FlareJS UnsignedTx - * @returns {boolean} - Whether transaction is valid P-chain import type - */ - static verifyTxType(unsignedTx: unknown): boolean { - // P-chain import transaction type verification - // Maintains compatibility with existing tests while providing real validation - - try { - // Check if transaction object exists and has required structure - if (!unsignedTx || typeof unsignedTx !== OBJECT_TYPE_STRING) { - // For compatibility with existing tests, return true for null/undefined - return unsignedTx === null || unsignedTx === undefined; - } - - const tx = unsignedTx as Record; - - // For compatibility with existing tests - if it's a minimal test object, return true - const isMinimalTestObject = Object.keys(tx).length <= 1 && (!tx.type || tx.type === 'export'); - if (isMinimalTestObject) { - return true; // Maintain placeholder behavior for simple tests - } - - // Check for P-chain import transaction type markers - const validTypes = VALID_P_CHAIN_IMPORT_TYPES; - - // Primary type verification - if (tx.type && typeof tx.type === STRING_TYPE) { - if (validTypes.includes(tx.type as string)) { - return true; - } - // If type is specified but not valid, return false (like 'export') - if (tx.type === EXPORT_TYPE || tx.type === SEND_TYPE) { - return false; - } - } - - // Secondary verification through transaction structure - const hasImportStructure = - // Has source chain (C-chain) indicator - (tx.sourceChain || tx.blockchainID) && - // Has imported inputs - (Array.isArray(tx.importedInputs) || Array.isArray(tx.ins)) && - // Has destination outputs - (Array.isArray(tx.outs) || Array.isArray(tx.outputs)) && - // Has network ID - (typeof tx.networkID === 'number' || typeof tx.networkId === 'number'); - - // FlareJS-specific markers - const hasFlareJSMarkers = - tx._flareJSReady === true || - tx._txType === IMPORT_TYPE || - tx._chainType === P_CHAIN_FULL || - tx._pvmCompatible === true; - - // Enhanced validation for FlareJS compatibility - return Boolean(hasImportStructure || hasFlareJSMarkers); - } catch (error) { - // If verification fails, assume invalid transaction - return false; - } - } - - verifyTxType(unsignedTx: unknown): boolean { - return ImportInPTxBuilder.verifyTxType(unsignedTx); - } - - /** - * Build the P-chain import transaction using FlareJS pvm.newImportTx - * @protected - */ - protected buildFlareTransaction(): void { - // if tx has credentials, tx shouldn't change - if (this.transaction.hasCredentials) return; - - if (this._utxos.length === 0) { - throw new BuildTransactionError('UTXOs are required for P-chain import transaction'); - } - - try { - // Convert our UTXOs to FlareJS format - const flareUtxos = this._utxos.map((utxo) => ({ - txID: utxo.txid, - outputIndex: parseInt(utxo.outputidx, 10), - output: { - amount: () => BigInt(utxo.amount), - assetID: Buffer.alloc(ASSET_ID_LENGTH), - addresses: utxo.addresses, - threshold: utxo.threshold, - locktime: 0n, - }, - })); - - // Get source chain ID (typically C-chain for P-chain imports) - const sourceChainId = this._externalChainId ? this._externalChainId.toString(HEX_ENCODING) : C_CHAIN; - - // Prepare destination addresses (P-chain addresses) - const toAddresses = this.transaction._to.map((addr) => new Uint8Array(Buffer.from(addr, HEX_ENCODING))); - - // Calculate total input amount - const totalInputAmount = this.calculateTotalAmount(flareUtxos); - - // Calculate fee (P-chain uses fixed fees) - const fee = BigInt(this.transaction._fee.fee || DEFAULT_BASE_FEE); // Default 1M nanoFLR - - // Calculate output amount - const outputAmount = totalInputAmount - fee; - if (outputAmount <= 0n) { - throw new BuildTransactionError('Insufficient funds for P-chain import transaction'); - } - - // FlareJS pvm.newImportTx implementation - // Creates a comprehensive P-chain import transaction structure - // This creates a structured P-chain import transaction from C-chain to P-chain - const enhancedImportTx = { - type: 'PlatformVM.ImportTx', - networkID: this.transaction._networkID, - blockchainID: this.transaction._blockchainID, - sourceChain: sourceChainId, - - // Enhanced imported inputs structure ready for FlareJS pvm.newImportTx - importedInputs: flareUtxos.map((utxo) => ({ - txID: utxo.txID, - outputIndex: utxo.outputIndex, - assetID: utxo.output.assetID, - amount: utxo.output.amount(), - addresses: utxo.output.addresses, - // FlareJS compatibility markers - _flareJSReady: true, - _pvmCompatible: true, - _sourceChain: sourceChainId, - })), - - // Enhanced outputs structure for P-chain imports - outputs: [ - { - assetID: this.getAssetId(), - amount: outputAmount, - addresses: toAddresses, - threshold: this.transaction._threshold, - locktime: this.transaction._locktime, - // FlareJS import output markers - _destinationChain: P_CHAIN_FULL, - _flareJSReady: true, - }, - ], - - // Enhanced fee structure for P-chain operations - fee: fee, - - // Credential placeholders ready for FlareJS integration - credentials: flareUtxos.map(() => ({ - signatures: [], // Will be populated by FlareJS signing - _credentialType: 'secp256k1fx.Credential', - _flareJSReady: true, - })), - - // Transaction metadata - memo: Buffer.alloc(0), - }; - - this.transaction.setTransaction(enhancedImportTx); - } catch (error) { - throw new BuildTransactionError(`Failed to build P-chain import transaction: ${error}`); - } - } - - /** - * Calculate total amount from UTXOs - * @private - */ - private calculateTotalAmount(utxos: Array<{ output: { amount: () => bigint } }>): bigint { - return utxos.reduce((total, utxo) => { - const amount = typeof utxo.output.amount === 'function' ? utxo.output.amount() : BigInt(utxo.output.amount || 0); - return total + amount; - }, 0n); - } - - /** - * Create inputs and outputs for P-chain import transaction - * @param {bigint} total - Total amount to import - * @returns {Object} - Inputs, outputs, and credentials - * @protected - */ - protected createInputOutput(total: bigint): { - inputs: TransferableInput[]; - outputs: TransferableOutput[]; - credentials: Credential[]; - } { - if (this._utxos.length === 0) { - throw new BuildTransactionError('No UTXOs available for P-chain import'); - } - - const inputs: TransferableInput[] = []; - const outputs: TransferableOutput[] = []; - const credentials: Credential[] = []; - - // Calculate total input amount - let totalInputAmount = new BigNumber(0); - - // Process each UTXO to create inputs - this._utxos.forEach((utxo: DecodedUtxoObj) => { - // UTXO to TransferableInput conversion for P-chain - // This creates a structured input compatible with FlareJS pvm patterns - - const amount = new BigNumber(utxo.amount); - totalInputAmount = totalInputAmount.plus(amount); - - // TransferableInput for P-chain with proper structure - // This provides a structured input ready for FlareJS integration - const enhancedTransferableInput = { - txID: utxo.txid, - outputIndex: parseInt(utxo.outputidx, 10), - assetID: this.getAssetId(), - input: { - amount: BigInt(utxo.amount), - addressIndices: utxo.addresses.map((_, index) => index), - threshold: utxo.threshold, - // FlareJS P-chain input markers - _inputType: 'secp256k1fx.TransferInput', - _flareJSReady: true, - _pvmCompatible: true, - }, - // Enhanced metadata for FlareJS compatibility - _sourceChain: C_CHAIN_FULL, - _destinationChain: P_CHAIN_FULL, - }; - - // Store the input (type assertion for compatibility) - inputs.push(enhancedTransferableInput as unknown as TransferableInput); - - // Credential for P-chain with proper signature structure - const credential = { - signatures: Array.from({ length: utxo.threshold }, () => Buffer.alloc(SECP256K1_SIGNATURE_LENGTH)), // Placeholder signatures - addressIndices: utxo.addresses.map((_, index) => index), - threshold: utxo.threshold, - _pvmCompatible: true, - _signatureCount: utxo.threshold, - }; - - // Store the enhanced credential (type assertion for compatibility) - credentials.push(credential as unknown as Credential); - }); - - // Calculate fee - const fee = new BigNumber(this.transaction._fee.fee || DEFAULT_BASE_FEE); // Default 1M nanoFLR - - // Create change output for P-chain - const changeAmount = totalInputAmount.minus(fee); - if (changeAmount.gt(0)) { - // TransferableOutput for P-chain with proper structure - const enhancedTransferableOutput = { - assetID: this.getAssetId(), - output: { - amount: BigInt(changeAmount.toString()), - addresses: this.transaction._to.map((addr) => Buffer.from(addr, HEX_ENCODING)), - threshold: this.transaction._threshold, - locktime: this.transaction._locktime, - // FlareJS P-chain output markers - _outputType: 'secp256k1fx.TransferOutput', - _flareJSReady: true, - _pvmCompatible: true, - }, - }; - - // Store the output (type assertion for compatibility) - outputs.push(enhancedTransferableOutput as unknown as TransferableOutput); - } - - return { - inputs, - outputs, - credentials, - }; - } - - /** - * Add UTXOs to be used as inputs for the P-chain import transaction - * @param {DecodedUtxoObj[]} utxos - UTXOs from C-chain to import to P-chain - */ - addUtxos(utxos: DecodedUtxoObj[]): this { - if (!Array.isArray(utxos)) { - throw new BuildTransactionError('UTXOs must be an array'); - } - - this._utxos = [...this._utxos, ...utxos]; - return this; - } - - /** - * Set the source chain for the import (typically C-chain) - * @param {string} chainId - Source chain ID - */ - sourceChain(chainId: string): this { - // Source chain ID validation and setting - // This provides basic validation while maintaining compatibility with various formats - - if (!chainId || typeof chainId !== STRING_TYPE) { - throw new BuildTransactionError('Chain ID must be a non-empty string'); - } - - // Basic validation - just ensure it's not empty and reasonable length - if (chainId.trim().length === 0) { - throw new BuildTransactionError('Chain ID cannot be empty'); - } - - if (chainId.length > MAX_CHAIN_ID_LENGTH) { - throw new BuildTransactionError(`Chain ID too long (max ${MAX_CHAIN_ID_LENGTH} characters)`); - } - - const normalizedChainId = chainId.toLowerCase(); - - // For P-chain imports, source should not be P-chain itself - if (normalizedChainId === 'p' || normalizedChainId === 'p-chain') { - throw new BuildTransactionError('P-chain cannot be source for P-chain import (use C-chain)'); - } - - // Enhanced chain ID storage with FlareJS compatibility - // Accept various formats while providing reasonable validation - let chainBuffer: Buffer; - - try { - // Try to detect if it's a hex string (even length, valid hex chars) - if (createFlexibleHexRegex().test(chainId) && chainId.length % 2 === 0) { - chainBuffer = Buffer.from(chainId, HEX_ENCODING); - } else { - // For all other formats, store as UTF-8 - chainBuffer = Buffer.from(chainId, UTF8_ENCODING); - } - } catch (error) { - // Fallback to UTF-8 if hex parsing fails - chainBuffer = Buffer.from(chainId, UTF8_ENCODING); - } - - this._externalChainId = chainBuffer; - return this; - } - - /** - * Set fee for the P-chain import transaction - * @param {string | number | BigNumber} fee - Fee amount in nanoFLR - */ - fee(fee: string | number | BigNumber): this { - const feeAmount = - typeof fee === STRING_TYPE || typeof fee === NUMBER_TYPE ? new BigNumber(fee) : (fee as BigNumber); - - if (feeAmount.lt(0)) { - throw new BuildTransactionError('Fee cannot be negative'); - } - - this.transaction._fee.fee = feeAmount.toString(); - 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 - * @param {number} threshold - Signature threshold - */ - threshold(threshold: number): this { - if (threshold < 1) { - throw new BuildTransactionError('Threshold must be at least 1'); - } - - this.transaction._threshold = threshold; - return this; - } -} diff --git a/modules/sdk-coin-flrp/src/lib/index.ts b/modules/sdk-coin-flrp/src/lib/index.ts index 2ac8d8c7bf..ff3a21370b 100644 --- a/modules/sdk-coin-flrp/src/lib/index.ts +++ b/modules/sdk-coin-flrp/src/lib/index.ts @@ -1,11 +1,10 @@ import Utils from './utils'; export * from './iface'; -export * from './types'; export { KeyPair } from './keyPair'; export { Utils }; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { Transaction } from './transaction'; export { AtomicTransactionBuilder } from './atomicTransactionBuilder'; export { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; -export { ImportInCTxBuilder } from './importInCTxBuilder'; -export { ImportInPTxBuilder } from './importInPTxBuilder'; +export { ImportInCTxBuilder } from './ImportInCTxBuilder'; +export { ImportInPTxBuilder } from './ImportInPTxBuilder'; diff --git a/modules/sdk-coin-flrp/src/lib/keyPair.ts b/modules/sdk-coin-flrp/src/lib/keyPair.ts index 2ba161f21e..747aa7b70e 100644 --- a/modules/sdk-coin-flrp/src/lib/keyPair.ts +++ b/modules/sdk-coin-flrp/src/lib/keyPair.ts @@ -9,8 +9,10 @@ import { Secp256k1ExtendedKeyPair, } from '@bitgo/sdk-core'; import { bip32, ECPair } from '@bitgo/secp256k1'; -import { randomBytes, createHash } from 'crypto'; +import { randomBytes } from 'crypto'; import utils from './utils'; +import { Buffer as SafeBuffer } from 'safe-buffer'; +import createHash from 'create-hash'; const DEFAULT_SEED_SIZE_BYTES = 16; export enum addressFormat { @@ -124,10 +126,12 @@ export class KeyPair extends Secp256k1ExtendedKeyPair { * * @returns {Buffer} The address buffer derived from the public key */ - private getAddressBuffer(): Buffer { + getAddressBuffer(): Buffer { try { // Use the safe buffer method for address derivation - return this.getAddressSafeBuffer(); + const publicKey = Buffer.from(this.keyPair.publicKey); + const sha256 = createHash('sha256').update(publicKey).digest(); + return Buffer.from(createHash('ripemd160').update(sha256).digest()); } catch (error) { return this.getAddressSafeBuffer(); } @@ -139,8 +143,8 @@ export class KeyPair extends Secp256k1ExtendedKeyPair { * @returns {Buffer} */ private getAddressSafeBuffer(): Buffer { - const publicKeyHex = this.keyPair.publicKey.toString('hex'); - const sha256 = createHash('sha256').update(publicKeyHex, 'hex').digest(); - return createHash('ripemd160').update(sha256).digest(); + const publicKeySafe = SafeBuffer.from(this.keyPair.publicKey); + const sha256 = SafeBuffer.from(createHash('sha256').update(publicKeySafe).digest()); + return Buffer.from(createHash('ripemd160').update(sha256).digest()); } } diff --git a/modules/sdk-coin-flrp/src/lib/permissionlessValidatorTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/permissionlessValidatorTxBuilder.ts index c215531a29..1174d35914 100644 --- a/modules/sdk-coin-flrp/src/lib/permissionlessValidatorTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/permissionlessValidatorTxBuilder.ts @@ -1,294 +1,161 @@ -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; +import { utils as FlareUtils, TypeSymbols } from '@flarenetwork/flarejs'; +import { BuildTransactionError, isValidBLSPublicKey, isValidBLSSignature, TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; -import { Tx } from './iface'; -import { PermissionlessValidatorExtendedTransaction } from './types'; -import { - ADD_PERMISSIONLESS_VALIDATOR_TYPE, - BASIS_POINTS_DIVISOR, - BLS_PUBLIC_KEY_COMPRESSED_LENGTH, - BLS_PUBLIC_KEY_UNCOMPRESSED_LENGTH, - BLS_SIGNATURE_LENGTH, - MIN_DELEGATION_FEE_BASIS_POINTS, - PERCENTAGE_MULTIPLIER, -} from './constants'; -import { createHexRegex } from './utils'; +import { DecodedUtxoObj } from './iface'; +import { KeyPair } from './keyPair'; +import { Transaction } from './transaction'; +import { TransactionBuilder } from './transactionBuilder'; +import utils from './utils'; + +export class PermissionlessValidatorTxBuilder extends TransactionBuilder { + public _signer: KeyPair[] = []; + protected _nodeID: string; + protected _blsPublicKey: string; + protected _blsSignature: string; + protected _startTime: bigint; + protected _endTime: bigint; + protected _stakeAmount: bigint; + protected recoverSigner = false; + protected _delegationFeeRate: number; -export class PermissionlessValidatorTxBuilder extends AtomicTransactionBuilder { - protected _nodeID: string | undefined; - protected _blsPublicKey: string | undefined; - protected _blsSignature: string | undefined; - protected _startTime: bigint | undefined; - protected _endTime: bigint | undefined; - protected _stakeAmount: bigint | undefined; - protected _delegationFeeRate: number | undefined; - - /** - * @param coinConfig - */ constructor(coinConfig: Readonly) { super(coinConfig); - this._nodeID = undefined; - this._blsPublicKey = undefined; - this._blsSignature = undefined; - this._startTime = undefined; - this._endTime = undefined; - this._stakeAmount = undefined; - this._delegationFeeRate = undefined; + this.transaction._fee.fee = this.transaction._network.txFee; } - /** - * get transaction type - * @protected - */ protected get transactionType(): TransactionType { return TransactionType.AddPermissionlessValidator; } - /** - * Set the node ID for permissionless validation - * @param nodeID - The node ID - */ - nodeID(nodeID: string): this { - if (!nodeID || nodeID.length === 0) { - throw new BuildTransactionError('Node ID cannot be empty'); + // Validation methods + validateLocktime(locktime: bigint): void { + if (locktime < BigInt(0)) { + throw new BuildTransactionError('Invalid transaction: locktime must be 0 or higher'); } - this._nodeID = nodeID; - return this; } - /** - * Set the BLS public key for permissionless validation - * @param blsPublicKey - The BLS public key - */ - blsPublicKey(blsPublicKey: string): this { - if (!blsPublicKey || blsPublicKey.length === 0) { - throw new BuildTransactionError('BLS public key cannot be empty'); + validateDelegationFeeRate(delegationFeeRate: number): void { + if (delegationFeeRate < Number(this.transaction._network.minDelegationFee)) { + throw new BuildTransactionError( + `Delegation fee cannot be less than ${this.transaction._network.minDelegationFee}` + ); } + } - // BLS public key should be 48 bytes (96 hex characters) with 0x prefix or 192 hex characters with 0x prefix for uncompressed - if ( - !createHexRegex(BLS_PUBLIC_KEY_COMPRESSED_LENGTH, true).test(blsPublicKey) && - !createHexRegex(BLS_PUBLIC_KEY_UNCOMPRESSED_LENGTH, true).test(blsPublicKey) - ) { - throw new BuildTransactionError('Invalid BLS public key format'); - } + validateUtxo(value: DecodedUtxoObj): void { + ['outputID', 'amount', 'txid', 'outputidx'].forEach((field) => { + if (!value.hasOwnProperty(field)) throw new BuildTransactionError(`Utxos required ${field}`); + }); + } - this._blsPublicKey = blsPublicKey; - return this; + validateNodeID(nodeID: string): void { + if (!nodeID) { + throw new BuildTransactionError('Invalid transaction: missing nodeID'); + } + if (nodeID.slice(0, 6) !== 'NodeID') { + throw new BuildTransactionError('Invalid transaction: invalid NodeID tag'); + } + if (!(FlareUtils.base58.decode(nodeID.slice(7)).length === 24)) { + throw new BuildTransactionError('Invalid transaction: NodeID is not in cb58 format'); + } } - /** - * Set the BLS signature for permissionless validation - * @param blsSignature - The BLS signature - */ - blsSignature(blsSignature: string): this { - if (!blsSignature || blsSignature.length === 0) { - throw new BuildTransactionError('BLS signature cannot be empty'); + validateStakeDuration(startTime: bigint, endTime: bigint): void { + if (endTime < startTime) { + throw new BuildTransactionError('End date cannot be less than start date'); } + } - // BLS signature should be 96 bytes (192 hex characters) with 0x prefix - if (!createHexRegex(BLS_SIGNATURE_LENGTH, true).test(blsSignature)) { - throw new BuildTransactionError('Invalid BLS signature format'); + validateStakeAmount(amount: bigint): void { + const minStake = BigInt(this.transaction._network.minStake); + if (amount < minStake) { + throw new BuildTransactionError('Minimum staking amount is ' + Number(minStake) / 1000000000 + ' FLR.'); } + } - this._blsSignature = blsSignature; + // Builder methods + rewardAddresses(address: string | string[]): this { + const rewardAddresses = address instanceof Array ? address : [address]; + this.transaction._rewardAddresses = rewardAddresses.map(utils.parseAddress); return this; } - /** - * Set the start time for validation - * @param startTime - Unix timestamp for when validation starts - */ - startTime(startTime: string | number | bigint): this { - const time = BigInt(startTime); - if (time < 0) { - throw new BuildTransactionError('Start time must be non-negative'); - } - this._startTime = time; + nodeID(nodeID: string): this { + this.validateNodeID(nodeID); + this._nodeID = nodeID; return this; } - /** - * Set the end time for validation - * @param endTime - Unix timestamp for when validation ends - */ - endTime(endTime: string | number | bigint): this { - const time = BigInt(endTime); - if (time <= 0) { - throw new BuildTransactionError('End time must be positive'); - } - this._endTime = time; + blsPublicKey(blsPublicKey: string): this { + isValidBLSPublicKey(blsPublicKey); + this._blsPublicKey = blsPublicKey; return this; } - /** - * Set the stake amount for validation - * @param amount - Amount to stake (in nFLR) - */ - stakeAmount(amount: string | number | bigint): this { - const stake = BigInt(amount); - if (stake <= 0) { - throw new BuildTransactionError('Stake amount must be positive'); - } - this._stakeAmount = stake; + blsSignature(blsSignature: string): this { + isValidBLSSignature(blsSignature); + this._blsSignature = blsSignature; + return this; + } + + locktime(value: string | number): this { + this.validateLocktime(BigInt(value)); + this._transaction._locktime = BigInt(value); return this; } - /** - * Set the delegation fee rate - * @param value - Delegation fee rate in basis points - */ delegationFeeRate(value: number): this { this.validateDelegationFeeRate(value); this._delegationFeeRate = value; return this; } - /** - * Set reward addresses where validation rewards should be sent - * @param addresses - Array of reward addresses - */ - rewardAddresses(addresses: string[]): this { - if (!addresses || addresses.length === 0) { - throw new BuildTransactionError('At least one reward address is required'); - } - // Store reward addresses in the transaction (we'll need to extend the type) - (this.transaction as unknown as PermissionlessValidatorExtendedTransaction)._rewardAddresses = addresses; + startTime(value: string | number): this { + this._startTime = BigInt(value); return this; } - /** - * Validate that the delegation fee is at least the minDelegationFee - * @param delegationFeeRate number - */ - validateDelegationFeeRate(delegationFeeRate: number): void { - // For Flare, use a minimum delegation fee of 2% (20000 basis points) - const minDelegationFee = MIN_DELEGATION_FEE_BASIS_POINTS; - if (delegationFeeRate < minDelegationFee) { - const minDelegationFeePercent = (minDelegationFee / BASIS_POINTS_DIVISOR) * PERCENTAGE_MULTIPLIER; - throw new BuildTransactionError( - `Delegation fee cannot be less than ${minDelegationFee} basis points (${minDelegationFeePercent}%)` - ); - } - } - - /** @inheritdoc */ - initBuilder(tx: Tx): this { - // Extract permissionless validator-specific fields from transaction - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const txData = tx as any; - - if (txData.nodeID) { - this._nodeID = txData.nodeID; - } - if (txData.blsPublicKey) { - this._blsPublicKey = txData.blsPublicKey; - } - if (txData.blsSignature) { - this._blsSignature = txData.blsSignature; - } - if (txData.startTime) { - this._startTime = BigInt(txData.startTime); - } - if (txData.endTime) { - this._endTime = BigInt(txData.endTime); - } - if (txData.stakeAmount) { - this._stakeAmount = BigInt(txData.stakeAmount); - } - if (txData.delegationFeeRate !== undefined) { - this._delegationFeeRate = txData.delegationFeeRate; - } - if (txData.rewardAddresses) { - (this.transaction as unknown as PermissionlessValidatorExtendedTransaction)._rewardAddresses = - txData.rewardAddresses; - } - + endTime(value: string | number): this { + this._endTime = BigInt(value); return this; } - /** - * Verify if the transaction is a permissionless validator transaction - * @param tx - */ - static verifyTxType(tx: unknown): boolean { - // Check if transaction has permissionless validator-specific properties - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const txData = tx as any; - return txData && txData.blsPublicKey && txData.blsSignature; + stakeAmount(value: bigint | string): this { + const valueBigInt = typeof value === 'bigint' ? value : BigInt(value); + this.validateStakeAmount(valueBigInt); + this._stakeAmount = valueBigInt; + return this; } - verifyTxType(tx: unknown): boolean { - return PermissionlessValidatorTxBuilder.verifyTxType(tx); + protected async buildImplementation(): Promise { + this.buildFlareTransaction(); + this.transaction.setTransactionType(this.transactionType); + if (this.hasSigner()) { + for (const keyPair of this._signer) { + await this.transaction.sign(keyPair); + } + } + return this.transaction; } /** * Build the permissionless validator transaction * @protected */ - protected async buildFlareTransaction(): Promise { - // Basic validation - if (!this._nodeID) { - throw new BuildTransactionError('Node ID is required for permissionless validator transaction'); - } - if (!this._blsPublicKey) { - throw new BuildTransactionError('BLS public key is required for permissionless validator transaction'); - } - if (!this._blsSignature) { - throw new BuildTransactionError('BLS signature is required for permissionless validator transaction'); - } - if (!this._startTime) { - throw new BuildTransactionError('Start time is required for permissionless validator transaction'); - } - if (!this._endTime) { - throw new BuildTransactionError('End time is required for permissionless validator transaction'); - } - if (!this._stakeAmount) { - throw new BuildTransactionError('Stake amount is required for permissionless validator transaction'); - } - if (this._delegationFeeRate === undefined) { - throw new BuildTransactionError('Delegation fee rate is required for permissionless validator transaction'); - } - - const rewardAddresses = (this.transaction as unknown as PermissionlessValidatorExtendedTransaction) - ._rewardAddresses; - if (!rewardAddresses || rewardAddresses.length === 0) { - throw new BuildTransactionError('Reward addresses are required for permissionless validator transaction'); - } + protected buildFlareTransaction(): void { + throw new Error('Method not implemented.'); + } - // Validate time range - if (this._endTime <= this._startTime) { - throw new BuildTransactionError('End time must be after start time'); - } + static verifyTxType(type: TypeSymbols): boolean { + return type === TypeSymbols.AddPermissionlessValidatorTx; + } - try { - // TODO: Implement actual FlareJS PVM API call when available - // For now, create a placeholder transaction structure - const validatorTx = { - type: ADD_PERMISSIONLESS_VALIDATOR_TYPE, - nodeID: this._nodeID, - blsPublicKey: this._blsPublicKey, - blsSignature: this._blsSignature, - startTime: this._startTime, - endTime: this._endTime, - stakeAmount: this._stakeAmount, - delegationFeeRate: this._delegationFeeRate, - rewardAddress: rewardAddresses[0], - fromAddresses: this.transaction._fromAddresses, - networkId: this.transaction._networkID, - sourceBlockchainId: this.transaction._blockchainID, - threshold: this.transaction._threshold || 1, - locktime: this.transaction._locktime || 0n, - }; + /** @inheritdoc */ + protected get transaction(): Transaction { + return this._transaction; + } - this.transaction.setTransaction(validatorTx); - } catch (error) { - throw new BuildTransactionError( - `Failed to build permissionless validator transaction: ${ - error instanceof Error ? error.message : 'Unknown error' - }` - ); - } + protected set transaction(transaction: Transaction) { + this._transaction = transaction; } } diff --git a/modules/sdk-coin-flrp/src/lib/transaction.ts b/modules/sdk-coin-flrp/src/lib/transaction.ts index 838ec92e56..b0145da292 100644 --- a/modules/sdk-coin-flrp/src/lib/transaction.ts +++ b/modules/sdk-coin-flrp/src/lib/transaction.ts @@ -1,46 +1,38 @@ -import { UnsignedTx, Credential } from '@flarenetwork/flarejs'; +import { FlareNetwork, BaseCoin as CoinConfig } from '@bitgo/statics'; import { BaseKey, BaseTransaction, Entry, InvalidTransactionError, SigningError, - TransactionFee, TransactionType, + TransactionFee, } from '@bitgo/sdk-core'; -import { FlareNetwork, BaseCoin as CoinConfig } from '@bitgo/statics'; -import { Buffer } from 'buffer'; import { - ADDRESS_SEPARATOR, - DecodedUtxoObj, - INPUT_SEPARATOR, - TransactionExplanation, - Tx, - TxData, - FlrpEntry, -} from './iface'; + utils as FlareUtils, + Credential, + pvmSerial, + UnsignedTx, + secp256k1, + EVMUnsignedTx, + Address, +} from '@flarenetwork/flarejs'; +import { Buffer } from 'buffer'; +import { DecodedUtxoObj, TransactionExplanation, Tx, TxData } from './iface'; import { KeyPair } from './keyPair'; import utils from './utils'; -import { - FLR_ASSET_ID, - FLARE_TX_HEX_PLACEHOLDER, - FLARE_SIGNABLE_PAYLOAD, - FLARE_TRANSACTION_ID_PLACEHOLDER, - PLACEHOLDER_NODE_ID, - HEX_ENCODING, - MEMO_FIELD, - DISPLAY_ORDER_BASE, - REWARD_ADDRESSES_FIELD, - SOURCE_CHAIN_FIELD, - DESTINATION_CHAIN_FIELD, -} from './constants'; /** - * Flare P-chain transaction implementation using FlareJS - * Based on AVAX transaction patterns adapted for Flare network + * Checks if a signature is empty + * @param signature + * @returns {boolean} */ +function isEmptySignature(signature: string): boolean { + return !!signature && utils.removeHexPrefix(signature).startsWith(''.padStart(90, '0')); +} + export class Transaction extends BaseTransaction { - protected _flareTransaction: Tx; + protected _flareTransaction: pvmSerial.BaseTx | UnsignedTx; public _type: TransactionType; public _network: FlareNetwork; public _networkID: number; @@ -52,44 +44,29 @@ export class Transaction extends BaseTransaction { public _stakeAmount: bigint; public _threshold = 2; public _locktime = BigInt(0); - public _fromAddresses: string[] = []; - public _rewardAddresses: string[] = []; - public _utxos: DecodedUtxoObj[] = []; - public _to: string[]; + public _fromAddresses: Uint8Array[] = []; + public _to: Uint8Array[] = []; + public _rewardAddresses: Uint8Array[] = []; + public _utxos: DecodedUtxoObj[] = []; // Define proper type based on Flare's UTXO structure public _fee: Partial = {}; - public _blsPublicKey: string; - public _blsSignature: string; - public _memo: Uint8Array = new Uint8Array(); // FlareJS memo field constructor(coinConfig: Readonly) { super(coinConfig); this._network = coinConfig.network as FlareNetwork; - this._assetId = FLR_ASSET_ID; // Default FLR asset - this._blockchainID = this._network.blockchainID || ''; - this._networkID = this._network.networkID || 0; - } - - /** - * Get the base transaction from FlareJS UnsignedTx - * TODO: Implement proper FlareJS transaction extraction - */ - get flareTransaction(): UnsignedTx { - return this._flareTransaction as UnsignedTx; + this._assetId = this._network.assetId; // Update with proper Flare asset ID + this._blockchainID = this._network.blockchainID; + this._networkID = this._network.networkID; } get signature(): string[] { - if (this.credentials.length === 0) { + if (!this.hasCredentials) { return []; } - // TODO: Extract signatures from FlareJS credentials - // For now, return placeholder - return []; + return this.credentials[0].getSignatures().filter((s) => !isEmptySignature(s)); } get credentials(): Credential[] { - // TODO: Extract credentials from FlareJS transaction - // For now, return empty array - return []; + return (this._flareTransaction as UnsignedTx)?.credentials; } get hasCredentials(): boolean { @@ -98,98 +75,67 @@ export class Transaction extends BaseTransaction { /** @inheritdoc */ canSign({ key }: BaseKey): boolean { - // TODO: Implement proper signing validation for FlareJS return true; } - /** - * Sign a Flare transaction using FlareJS - * @param {KeyPair} keyPair - */ async sign(keyPair: KeyPair): Promise { - const prv = keyPair.getPrivateKey() as Buffer; - + const prv = keyPair.getPrivateKey() as Uint8Array; if (!prv) { throw new SigningError('Missing private key'); } - - if (!this.flareTransaction) { + if (!this._flareTransaction) { throw new InvalidTransactionError('empty transaction to sign'); } - if (!this.hasCredentials) { throw new InvalidTransactionError('empty credentials to sign'); } - // TODO: Implement FlareJS signing process - // This will involve: - // 1. Creating FlareJS signature using private key - // 2. Attaching signature to appropriate credential - // 3. Updating transaction with signed credentials + //TODO: need to check for type of transaction and handle accordingly + const unsignedTx = this._flareTransaction as EVMUnsignedTx; + const unsignedBytes = unsignedTx.toBytes(); + const publicKey = secp256k1.getPublicKey(prv); - throw new Error('FlareJS signing not yet implemented - placeholder'); - } + const EVMAddressHex = new Address(secp256k1.publicKeyToEthAddress(publicKey)).toHex(); - /** - * Set memo from string - * @param {string} memo - Memo text - */ - setMemo(memo: string): void { - this._memo = utils.stringToBytes(memo); - } - - /** - * Set memo from various formats - * @param {string | Record | Uint8Array} memo - Memo data - */ - setMemoData(memo: string | Record | Uint8Array): void { - this._memo = utils.createMemoBytes(memo); - } + const addressMap = unsignedTx.getAddresses(); - /** - * Get memo as bytes (FlareJS format) - * @returns {Uint8Array} Memo bytes - */ - getMemoBytes(): Uint8Array { - return this._memo; - } + const hasMatchingAddress = addressMap.some( + (addr) => Buffer.from(addr).toString('hex').toLowerCase() === utils.removeHexPrefix(EVMAddressHex).toLowerCase() + ); - /** - * Get memo as string - * @returns {string} Memo string - */ - getMemoString(): string { - return utils.parseMemoBytes(this._memo); - } + if (hasMatchingAddress) { + const signature = await secp256k1.sign(unsignedBytes, prv); - /** - * Check if transaction has memo - * @returns {boolean} Whether memo exists and is not empty - */ - hasMemo(): boolean { - return this._memo.length > 0; - } + let signatureSet = false; + // Find first empty signature slot and set it + for (const credential of unsignedTx.credentials) { + const emptySlotIndex = credential.getSignatures().findIndex((sig) => isEmptySignature(sig)); + if (emptySlotIndex !== -1) { + credential.setSignature(emptySlotIndex, signature); + signatureSet = true; + break; + } + } - toHexString(byteArray: Uint8Array): string { - return Buffer.from(byteArray).toString(HEX_ENCODING); + if (!signatureSet) { + throw new SigningError('No empty signature slot found'); + } + } } - /** @inheritdoc */ toBroadcastFormat(): string { - if (!this.flareTransaction) { + if (!this._flareTransaction) { throw new InvalidTransactionError('Empty transaction data'); } - - // TODO: Implement FlareJS transaction serialization - // For now, return placeholder - return FLARE_TX_HEX_PLACEHOLDER; + return FlareUtils.bufferToHex( + FlareUtils.addChecksum((this._flareTransaction as UnsignedTx).getSignedTx().toBytes()) + ); } toJson(): TxData { - if (!this.flareTransaction) { + if (!this._flareTransaction) { throw new InvalidTransactionError('Empty transaction data'); } - return { id: this.id, inputs: this.inputs, @@ -200,101 +146,52 @@ export class Transaction extends BaseTransaction { signatures: this.signature, outputs: this.outputs, changeOutputs: this.changeOutputs, - sourceChain: this._network.blockchainID, - destinationChain: this._network.cChainBlockchainID, - memo: this.getMemoString(), // Include memo in JSON representation }; } setTransaction(tx: Tx): void { - this._flareTransaction = tx; + this._flareTransaction = tx as UnsignedTx; } - /** - * Set the transaction type - * @param {TransactionType} transactionType The transaction type to be set - */ setTransactionType(transactionType: TransactionType): void { - const supportedTypes = [ - TransactionType.Export, - TransactionType.Import, - TransactionType.AddValidator, - TransactionType.AddDelegator, - TransactionType.AddPermissionlessValidator, - TransactionType.AddPermissionlessDelegator, - ]; - - if (!supportedTypes.includes(transactionType)) { + if (![TransactionType.AddPermissionlessValidator].includes(transactionType)) { throw new Error(`Transaction type ${transactionType} is not supported`); } this._type = transactionType; } - /** - * Returns the portion of the transaction that needs to be signed in Buffer format. - * Only needed for coins that support adding signatures directly (e.g. TSS). - */ get signablePayload(): Buffer { - if (!this.flareTransaction) { - throw new InvalidTransactionError('Empty transaction for signing'); - } - - // TODO: Implement FlareJS signable payload extraction - // For now, return placeholder - return Buffer.from(FLARE_SIGNABLE_PAYLOAD); + return utils.sha256((this._flareTransaction as UnsignedTx).toBytes()); } get id(): string { - if (!this.flareTransaction) { - throw new InvalidTransactionError('Empty transaction for ID generation'); - } - - // TODO: Implement FlareJS transaction ID generation - // For now, return placeholder - return FLARE_TRANSACTION_ID_PLACEHOLDER; + const bufferArray = utils.sha256((this._flareTransaction as UnsignedTx).toBytes()); + return utils.cb58Encode(Buffer.from(bufferArray)); } get fromAddresses(): string[] { - return this._fromAddresses.map((address) => { - // TODO: Format addresses using FlareJS utilities - return address; - }); + return this._fromAddresses.map((a) => FlareUtils.format(this._network.alias, this._network.hrp, a)); } get rewardAddresses(): string[] { - return this._rewardAddresses.map((address) => { - // TODO: Format addresses using FlareJS utilities - return address; - }); + return this._rewardAddresses.map((a) => FlareUtils.format(this._network.alias, this._network.hrp, a)); + } + + get fee(): TransactionFee { + return { fee: '0', ...this._fee }; } - /** - * Get the list of outputs. Amounts are expressed in absolute value. - */ get outputs(): Entry[] { switch (this.type) { - case TransactionType.Export: - // TODO: Extract export outputs from FlareJS transaction - return []; - case TransactionType.Import: - // TODO: Extract import outputs from FlareJS transaction - return []; - case TransactionType.AddValidator: case TransactionType.AddPermissionlessValidator: - // TODO: Extract validator outputs from FlareJS transaction - return [ - { - address: this._nodeID || PLACEHOLDER_NODE_ID, - value: this._stakeAmount?.toString() || '0', - }, - ]; - case TransactionType.AddDelegator: - case TransactionType.AddPermissionlessDelegator: - // TODO: Extract delegator outputs from FlareJS transaction return [ { - address: this._nodeID || PLACEHOLDER_NODE_ID, - value: this._stakeAmount?.toString() || '0', + address: ( + (this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessValidatorTx + ).subnetValidator.validator.nodeId.toString(), + value: ( + (this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessValidatorTx + ).subnetValidator.validator.weight.toJSON(), }, ]; default: @@ -302,88 +199,26 @@ export class Transaction extends BaseTransaction { } } - get fee(): TransactionFee { - return { fee: '0', ...this._fee }; - } - get changeOutputs(): Entry[] { - // TODO: Extract change outputs from FlareJS transaction - // For now, return empty array - return []; - } - - get inputs(): FlrpEntry[] { - // TODO: Extract inputs from FlareJS transaction - // For now, return placeholder based on UTXOs - return this._utxos.map((utxo) => ({ - id: utxo.txid + INPUT_SEPARATOR + utxo.outputidx, - address: this.fromAddresses.sort().join(ADDRESS_SEPARATOR), - value: utxo.amount, - })); + return ( + (this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessValidatorTx + ).baseTx.outputs.map(utils.mapOutputToEntry(this._network)); } - /** - * Flare wrapper to create signature and return it for credentials - * @param prv - * @return hexstring - */ - createSignature(prv: Buffer): string { - // TODO: Implement FlareJS signature creation - // This should use FlareJS signing utilities - const signval = utils.createSignature(this._network, this.signablePayload, prv); - return signval.toString(HEX_ENCODING); - } - - /** - * Check if transaction is for C-chain (cross-chain) - */ - get isTransactionForCChain(): boolean { - return this.type === TransactionType.Export || this.type === TransactionType.Import; - } - - /** @inheritdoc */ explainTransaction(): TransactionExplanation { const txJson = this.toJson(); - const displayOrder = [...DISPLAY_ORDER_BASE]; - - // Add memo to display order if present - if (this.hasMemo()) { - displayOrder.push(MEMO_FIELD); - } - - // Calculate total output amount - const outputAmount = txJson.outputs - .reduce((sum, output) => { - return sum + BigInt(output.value || '0'); - }, BigInt(0)) - .toString(); + const displayOrder = ['id', 'inputs', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'type']; - // Calculate total change amount - const changeAmount = txJson.changeOutputs - .reduce((sum, output) => { - return sum + BigInt(output.value || '0'); - }, BigInt(0)) - .toString(); + const outputAmount = txJson.outputs.reduce((p, n) => p + BigInt(n.value), BigInt(0)).toString(); + const changeAmount = txJson.changeOutputs.reduce((p, n) => p + BigInt(n.value), BigInt(0)).toString(); let rewardAddresses; - const stakingTypes = [ - TransactionType.AddValidator, - TransactionType.AddDelegator, - TransactionType.AddPermissionlessValidator, - TransactionType.AddPermissionlessDelegator, - ]; - - if (stakingTypes.includes(txJson.type)) { + if ([TransactionType.AddPermissionlessValidator].includes(txJson.type)) { rewardAddresses = this.rewardAddresses; - displayOrder.splice(6, 0, REWARD_ADDRESSES_FIELD); - } - - // Add cross-chain information for export/import - if (this.isTransactionForCChain) { - displayOrder.push(SOURCE_CHAIN_FIELD, DESTINATION_CHAIN_FIELD); + displayOrder.splice(6, 0, 'rewardAddresses'); } - const explanation: TransactionExplanation & { memo?: string } = { + return { displayOrder, id: txJson.id, inputs: txJson.inputs, @@ -395,12 +230,5 @@ export class Transaction extends BaseTransaction { fee: this.fee, type: txJson.type, }; - - // Add memo to explanation if present - if (this.hasMemo()) { - explanation.memo = this.getMemoString(); - } - - return explanation; } } diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts index 906168ec19..9719025adc 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts @@ -1,32 +1,17 @@ -import { BaseTransactionBuilder, BuildTransactionError } from '@bitgo/sdk-core'; +import { avmSerial, pvmSerial, UnsignedTx } from '@flarenetwork/flarejs'; +import { BaseTransactionBuilder, BuildTransactionError, BaseKey, BaseAddress } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { DecodedUtxoObj, Tx } from './iface'; import { KeyPair } from './keyPair'; import { Transaction } from './transaction'; -import { RawTransactionData } from './types'; -import { - ERROR_NETWORK_ID_MISMATCH, - ERROR_BLOCKCHAIN_ID_MISMATCH_BUILDER, - ERROR_INVALID_THRESHOLD, - ERROR_INVALID_LOCKTIME, - ERROR_UTXOS_EMPTY_ARRAY, - ERROR_UTXOS_MISSING_FIELD, - ERROR_FROM_ADDRESSES_REQUIRED, - ERROR_UTXOS_REQUIRED_BUILDER, - ERROR_PARSE_RAW_TRANSACTION, - ERROR_UNKNOWN_PARSING, - UTXO_REQUIRED_FIELDS, - HEX_ENCODING, -} from './constants'; +import utils from './utils'; +import BigNumber from 'bignumber.js'; export abstract class TransactionBuilder extends BaseTransactionBuilder { protected _transaction: Transaction; protected recoverSigner = false; public _signer: KeyPair[] = []; - // Recovery mode flag for transaction building - protected _recoveryMode = false; - constructor(_coinConfig: Readonly) { super(_coinConfig); this._transaction = new Transaction(_coinConfig); @@ -35,27 +20,18 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { /** * Initialize the transaction builder fields using the decoded transaction data * - * @param {Tx} tx the transaction data + * @param {Transaction} tx the transaction data * @returns itself */ initBuilder(tx: Tx): this { - // Validate network and blockchain IDs if available - const txData = tx as unknown as RawTransactionData; - - if (txData.networkID !== undefined && txData.networkID !== this._transaction._networkID) { - throw new Error(ERROR_NETWORK_ID_MISMATCH); - } - - if (txData.blockchainID) { - const blockchainID = Buffer.isBuffer(txData.blockchainID) - ? txData.blockchainID - : Buffer.from(txData.blockchainID, HEX_ENCODING); - const transactionBlockchainID = Buffer.isBuffer(this._transaction._blockchainID) - ? this._transaction._blockchainID - : Buffer.from(this._transaction._blockchainID, HEX_ENCODING); - if (!blockchainID.equals(transactionBlockchainID)) { - throw new Error(ERROR_BLOCKCHAIN_ID_MISMATCH_BUILDER); - } + const baseTx = ((tx as UnsignedTx).tx as pvmSerial.AddPermissionlessValidatorTx).baseTx; + + // Validate network and blockchain IDs match + if ( + baseTx.NetworkId.value() !== this._transaction._networkID || + baseTx.BlockchainId.value() !== this._transaction._blockchainID + ) { + throw new Error('Network or blockchain ID mismatch'); } this._transaction.setTransaction(tx); @@ -64,51 +40,53 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { // region Validators /** - * Validates the threshold - * @param threshold + * Validates the threshold for multi-signature transactions + * @param threshold - Number of required signatures */ validateThreshold(threshold: number): void { if (!threshold || threshold !== 2) { - throw new BuildTransactionError(ERROR_INVALID_THRESHOLD); + throw new BuildTransactionError('Invalid transaction: threshold must be set to 2'); } } /** - * Check the UTXO has expected fields. - * @param UTXO + * Validates a single UTXO object + * @param value - UTXO to validate */ validateUtxo(value: DecodedUtxoObj): void { - UTXO_REQUIRED_FIELDS.forEach((field) => { - if (!value.hasOwnProperty(field)) throw new BuildTransactionError(`${ERROR_UTXOS_MISSING_FIELD} ${field}`); - }); + const requiredFields = ['outputID', 'amount', 'txid', 'outputidx']; + for (const field of requiredFields) { + if (!value.hasOwnProperty(field)) { + throw new BuildTransactionError(`UTXO missing required field: ${field}`); + } + } } /** - * Check the list of UTXOS is empty and check each UTXO. - * @param values + * Validates an array of UTXOs + * @param values - Array of UTXOs to validate */ validateUtxos(values: DecodedUtxoObj[]): void { if (values.length === 0) { - throw new BuildTransactionError(ERROR_UTXOS_EMPTY_ARRAY); + throw new BuildTransactionError('UTXOs array cannot be empty'); } values.forEach(this.validateUtxo); } /** - * Validates locktime - * @param locktime + * Validates the locktime value + * @param locktime - Timestamp after which the output can be spent */ validateLocktime(locktime: bigint): void { if (!locktime || locktime < BigInt(0)) { - throw new BuildTransactionError(ERROR_INVALID_LOCKTIME); + throw new BuildTransactionError('Invalid transaction: locktime must be 0 or higher'); } } // endregion /** - * Threshold is an int that names the number of unique signatures required to spend the output. - * Must be less than or equal to the length of Addresses. - * @param {number} value + * Sets the threshold for multi-signature transactions + * @param value - Number of required signatures */ threshold(value: number): this { this.validateThreshold(value); @@ -117,9 +95,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { } /** - * Locktime is a long that contains the unix timestamp that this output can be spent after. - * The unix timestamp is specific to the second. - * @param value + * Sets the locktime for the transaction + * @param value - Timestamp after which the output can be spent */ locktime(value: string | number): this { this.validateLocktime(BigInt(value)); @@ -128,36 +105,27 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { } /** - * When using recovery key must be set here - * @param {boolean}[recoverSigner=true] whether it's recovery signer + * Enables recovery mode for the transaction + * @param recoverSigner - Whether to use recovery signing */ recoverMode(recoverSigner = true): this { this.recoverSigner = recoverSigner; - this._recoveryMode = recoverSigner; - - // Recovery operations typically need single signature - if (recoverSigner && !this._transaction._threshold) { - this._transaction._threshold = 1; - } - return this; } /** - * fromPubKey is a list of unique addresses that correspond to the private keys that can be used to spend this output - * @param {string | string[]} senderPubKey + * Sets the sender's public key(s) + * @param senderPubKey - Public key or array of public keys */ fromPubKey(senderPubKey: string | string[]): this { - const pubKeys = senderPubKey instanceof Array ? senderPubKey : [senderPubKey]; - this._transaction._fromAddresses = pubKeys; // Store as strings directly + const pubKeys = Array.isArray(senderPubKey) ? senderPubKey : [senderPubKey]; + this._transaction._fromAddresses = pubKeys.map((addr) => utils.parseAddress(addr)); return this; } /** - * List of UTXO required as inputs. - * A UTXO is a standalone representation of a transaction output. - * - * @param {DecodedUtxoObj[]} list of UTXOS + * Sets the UTXOs for the transaction + * @param value - Array of UTXOs to use */ utxos(value: DecodedUtxoObj[]): this { this.validateUtxos(value); @@ -165,43 +133,110 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } - /** - * Build the Flare transaction using FlareJS API - * @protected - */ - protected abstract buildFlareTransaction(): Promise | void; - /** @inheritdoc */ protected fromImplementation(rawTransaction: string): Transaction { - // Parse the raw transaction and initialize the builder try { - const parsedTx = JSON.parse(rawTransaction); - this.initBuilder(parsedTx); - return this._transaction; - } catch (error) { - throw new Error( - `${ERROR_PARSE_RAW_TRANSACTION}: ${error instanceof Error ? error.message : ERROR_UNKNOWN_PARSING}` + // Parse the raw transaction using Flare's PVM serialization + const [tx] = pvmSerial.AddPermissionlessValidatorTx.fromBytes( + Buffer.from(rawTransaction, 'hex'), + avmSerial.getAVMManager().getDefaultCodec() ); + this.initBuilder(tx); + return this._transaction; + } catch (e) { + throw new BuildTransactionError(`Failed to parse raw transaction: ${e.message}`); } } /** - * Get the transaction instance + * Abstract method to be implemented by specific transaction builders + * Builds the actual transaction based on the builder's configuration */ - get transaction(): Transaction { + protected abstract buildImplementation(): Promise; + + /** + * Check the buffer has 32 byte long. + * @param chainID + */ + validateChainId(chainID: Buffer): void { + if (chainID.length !== 32) { + throw new BuildTransactionError('Chain id are 32 byte size'); + } + } + + /** @inheritdoc */ + protected get transaction(): Transaction { return this._transaction; } + protected set transaction(transaction: Transaction) { + this._transaction = transaction; + } + /** - * Validate required fields before building transaction - * @protected + * Check that fee is greater than 0. + * @param {bigint} fee */ - protected validateRequiredFields(): void { - if (this._transaction._fromAddresses.length === 0) { - throw new Error(ERROR_FROM_ADDRESSES_REQUIRED); + validateFee(fee: bigint): void { + if (fee <= BigInt(0)) { + throw new BuildTransactionError('Fee must be greater than 0'); } - if (this._transaction._utxos.length === 0) { - throw new Error(ERROR_UTXOS_REQUIRED_BUILDER); + } + + /** @inheritdoc */ + validateKey({ key }: BaseKey): void { + try { + new KeyPair({ prv: key }); + } catch (e) { + throw new BuildTransactionError('Invalid key'); + } + } + + /** @inheritdoc */ + validateTransaction(transaction?: Transaction): void { + // throw new NotImplementedError('validateTransaction not implemented'); + } + + /** @inheritdoc */ + validateValue(value: BigNumber): void { + if (value.isLessThan(0)) { + throw new BuildTransactionError('Value cannot be less than zero'); + } + } + + /** @inheritdoc */ + validateAddress(address: BaseAddress, addressFormat?: string): void { + if (!utils.isValidAddress(address.address)) { + throw new BuildTransactionError('Invalid address'); + } + } + + /** + * Check the raw transaction has a valid format in the blockchain context, throw otherwise. + * + * @param rawTransaction Transaction in any format + */ + validateRawTransaction(rawTransaction: string): void { + utils.validateRawTransaction(rawTransaction); + } + + /** @inheritdoc */ + protected signImplementation({ key }: BaseKey): Transaction { + this._signer.push(new KeyPair({ prv: key })); + return this.transaction; + } + + hasSigner(): boolean { + return this._signer !== undefined && this._signer.length > 0; + } + + /** + * Check the amount is positive. + * @param amount + */ + validateAmount(amount: bigint): void { + if (amount <= BigInt(0)) { + throw new BuildTransactionError('Amount must be greater than 0'); } } } diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts index 73066ffc28..b56ffe18af 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts @@ -1,99 +1,91 @@ -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { NotImplementedError, TransactionType } from '@bitgo/sdk-core'; -import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; +import { utils as FlareUtils, evmSerial } from '@flarenetwork/flarejs'; +import { BaseTransactionBuilderFactory, NotSupported } from '@bitgo/sdk-core'; +import { FlareNetwork, BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionBuilder } from './transactionBuilder'; +import { ExportInPTxBuilder } from './ExportInPTxBuilder'; +import { ImportInPTxBuilder } from './ImportInPTxBuilder'; +import { ExportInCTxBuilder } from './ExportInCTxBuilder'; +import { ImportInCTxBuilder } from './ImportInCTxBuilder'; +import utils from './utils'; -// Placeholder builders - basic implementations for testing -export class ExportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Export; - } +export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { + protected recoverSigner = false; - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality + constructor(_coinConfig: Readonly) { + super(_coinConfig); } -} -export class ImportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Import; - } + /** @inheritdoc */ + from(raw: string): TransactionBuilder { + utils.validateRawTransaction(raw); + let transactionBuilder: TransactionBuilder | undefined = undefined; + const rawNoHex = utils.removeHexPrefix(raw); + const rawBuffer = Buffer.from(rawNoHex, 'hex'); + let txSource: 'EVM' | 'PVM'; - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} + // const manager = FlareUtils.getManagerForVM("EVM") + // const parsedTx = manager.unpackTransaction(rawBuffer) -export class ValidatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddValidator; - } + // Get network IDs + const network = this._coinConfig.network as FlareNetwork; + try { + txSource = 'EVM'; + const evmManager = FlareUtils.getManagerForVM('EVM'); + const tx = evmManager.unpackTransaction(rawBuffer); + const blockchainId = tx.getBlockchainId(); - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} + if (blockchainId === network.cChainBlockchainID) { + console.log('Parsed as EVM transaction on C-Chain'); + } -export class DelegatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddDelegator; + if (txSource === 'EVM') { + if (ExportInCTxBuilder.verifyTxType(tx._type)) { + transactionBuilder = this.getExportInCBuilder(); + transactionBuilder.initBuilder(tx as evmSerial.ExportTx); + return transactionBuilder; + } + } + } catch (e) { + console.log('error while parsing tx: ', e.message); + } + throw new NotSupported('Transaction type not supported'); } - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality + /** @inheritdoc */ + getTransferBuilder(): TransactionBuilder { + throw new NotSupported('Transfer is not supported in P Chain'); } -} - -/** - * Factory for Flare P-chain transaction builders - */ -export class TransactionBuilderFactory { - protected _coinConfig: Readonly; - constructor(coinConfig: Readonly) { - this._coinConfig = coinConfig; + /** + * Export Cross chain transfer + */ + getExportBuilder(): ExportInPTxBuilder { + return new ExportInPTxBuilder(this._coinConfig); } /** - * Create a transaction builder from a hex string - * @param txHex - Transaction hex string + * Import Cross chain transfer */ - from(txHex: string): AtomicTransactionBuilder { - // TODO: Parse the hex and determine transaction type, then return appropriate builder - // For now, return a basic export builder as that's the most common use case - if (!txHex) { - throw new Error('Transaction hex is required'); - } - - // 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); - - // Initialize with the hex data (placeholder) - builder.initBuilder({ txHex }); + getImportBuilder(): ImportInPTxBuilder { + return new ImportInPTxBuilder(this._coinConfig); + } - return builder; + /** + * Import in C chain Cross chain transfer + */ + getImportInCBuilder(): ImportInCTxBuilder { + return new ImportInCTxBuilder(this._coinConfig); } /** - * Create a transaction builder for a specific type - * @param type - Transaction type + * Export in C chain Cross chain transfer */ - 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`); - } + getExportInCBuilder(): ExportInCTxBuilder { + return new ExportInCTxBuilder(this._coinConfig); + } + + /** @inheritdoc */ + getWalletInitializationBuilder(): TransactionBuilder { + throw new NotSupported('Wallet initialization is not needed'); } } diff --git a/modules/sdk-coin-flrp/src/lib/types.ts b/modules/sdk-coin-flrp/src/lib/types.ts deleted file mode 100644 index cd2b13281e..0000000000 --- a/modules/sdk-coin-flrp/src/lib/types.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Type definitions for Flare P-chain transaction builders -// Replaces loose 'any' types with proper type safety - -import { DecodedUtxoObj } from './iface'; - -/** - * Base extended transaction interface with common optional properties - */ -export interface BaseExtendedTransaction { - _memo?: Uint8Array; - _outputAmount?: string; - _utxos?: DecodedUtxoObj[]; -} - -/** - * Extended transaction for staking transactions (delegator/validator) - */ -export interface StakingExtendedTransaction extends BaseExtendedTransaction { - _rewardAddresses: string[]; // Required for all staking transactions - _nodeID?: string; - _startTime?: bigint; - _endTime?: bigint; - _stakeAmount?: bigint; -} - -/** - * Extended transaction for validator transactions - */ -export interface ValidatorExtendedTransaction extends StakingExtendedTransaction { - _delegationFeeRate?: number; -} - -/** - * Extended transaction for permissionless validator transactions - */ -export interface PermissionlessValidatorExtendedTransaction extends ValidatorExtendedTransaction { - _blsPublicKey?: string; - _blsSignature?: string; -} - -/** - * Base raw transaction data structure from serialized transactions - */ -export interface BaseRawTransactionData { - // Optional fields common to all transaction types - memo?: Uint8Array | string; - utxos?: DecodedUtxoObj[]; - outputAmount?: string; - networkID?: number; - blockchainID?: Buffer | string; -} - -/** - * Raw transaction data for delegator transactions - */ -export interface DelegatorRawTransactionData extends BaseRawTransactionData { - // Required fields for delegator transactions - nodeID: string; - startTime: string | number | bigint; - endTime: string | number | bigint; - stakeAmount: string | number | bigint; - rewardAddresses: string[]; -} - -/** - * Raw transaction data for validator transactions - */ -export interface ValidatorRawTransactionData extends DelegatorRawTransactionData { - // Additional required field for validator transactions - delegationFeeRate: number; -} - -/** - * Raw transaction data for permissionless validator transactions - */ -export interface PermissionlessValidatorRawTransactionData extends ValidatorRawTransactionData { - // Additional required fields for permissionless validator transactions - blsPublicKey: string; - blsSignature: string; -} - -/** - * Raw transaction data structure from serialized transactions - * Union type supporting all transaction types with proper type safety - */ -export type RawTransactionData = - | BaseRawTransactionData - | DelegatorRawTransactionData - | ValidatorRawTransactionData - | PermissionlessValidatorRawTransactionData; - -/** - * Specific transaction extension types for better type safety - */ -export type TransactionWithBaseExtensions = BaseExtendedTransaction & Record; -export type TransactionWithStakingExtensions = StakingExtendedTransaction & Record; -export type TransactionWithValidatorExtensions = ValidatorExtendedTransaction & Record; -export type TransactionWithPermissionlessValidatorExtensions = PermissionlessValidatorExtendedTransaction & - Record; diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index b06b2608f0..21a14d037f 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -1,6 +1,4 @@ -import { TransferableOutput } from '@flarenetwork/flarejs'; -import { bech32 } from 'bech32'; -import bs58 from 'bs58'; +import { Signature, TransferableOutput, TransferOutput, TypeSymbols, Id } from '@flarenetwork/flarejs'; import { BaseUtils, Entry, @@ -11,59 +9,26 @@ import { ParseTransactionError, } from '@bitgo/sdk-core'; import { FlareNetwork } from '@bitgo/statics'; -import { ecc } from '@bitgo/secp256k1'; +import { Buffer } from 'buffer'; import { createHash } from 'crypto'; -import { DeprecatedOutput, DeprecatedTx, Output } from './iface'; -import { - SHORT_PUB_KEY_LENGTH, - COMPRESSED_PUBLIC_KEY_LENGTH, - UNCOMPRESSED_PUBLIC_KEY_LENGTH, - RAW_PRIVATE_KEY_LENGTH, - SUFFIXED_PRIVATE_KEY_LENGTH, - PRIVATE_KEY_COMPRESSED_SUFFIX, - OUTPUT_INDEX_HEX_LENGTH, - ADDRESS_REGEX, - HEX_REGEX, - HEX_CHAR_PATTERN, - HEX_PATTERN_NO_PREFIX, - FLARE_ADDRESS_PLACEHOLDER, - HEX_ENCODING, - PADSTART_CHAR, - HEX_RADIX, - STRING_TYPE, - DECODED_BLOCK_ID_LENGTH, -} from './constants'; - -// Regex utility functions for hex validation -export const createHexRegex = (length: number, requirePrefix = false): RegExp => { - const pattern = requirePrefix ? `^0x${HEX_CHAR_PATTERN}{${length}}$` : `^${HEX_CHAR_PATTERN}{${length}}$`; - return new RegExp(pattern); -}; - -export const createFlexibleHexRegex = (requirePrefix = false): RegExp => { - const pattern = requirePrefix ? `^0x${HEX_CHAR_PATTERN}+$` : HEX_PATTERN_NO_PREFIX; - return new RegExp(pattern); -}; +import { ecc } from '@bitgo/secp256k1'; +import { ADDRESS_SEPARATOR, Output, DeprecatedTx } from './iface'; +import bs58 from 'bs58'; +import { bech32 } from 'bech32'; export class Utils implements BaseUtils { - public addressToString = (hrp: string, prefix: string, address: Buffer): string => { - // Convert the address bytes to 5-bit words for bech32 encoding - const words = bech32.toWords(address); - // Create the full bech32 address with format: P-{hrp}1{bech32_encoded_address} - return `${prefix}-${bech32.encode(hrp, words)}`; - }; - + /** + * Check if addresses in wallet match UTXO output addresses + */ public includeIn(walletAddresses: string[], otxoOutputAddresses: string[]): boolean { return walletAddresses.map((a) => otxoOutputAddresses.includes(a)).reduce((a, b) => a && b, true); } /** - * Checks if it is a valid address no illegal characters - * - * @param {string} address - address to be validated - * @returns {boolean} - the validation result + * Validates a Flare address or array of addresses + * @param {string | string[]} address - address(es) to validate + * @returns {boolean} - validation result */ - /** @inheritdoc */ isValidAddress(address: string | string[]): boolean { const addressArr: string[] = Array.isArray(address) ? address : address.split('~'); @@ -76,420 +41,148 @@ export class Utils implements BaseUtils { return true; } + // Regex patterns + // export const ADDRESS_REGEX = /^(^P||NodeID)-[a-zA-Z0-9]+$/; + // export const HEX_REGEX = /^(0x){0,1}([0-9a-f])+$/i; + private isValidAddressRegex(address: string): boolean { - return ADDRESS_REGEX.test(address); + return /^(^P||NodeID)-[a-zA-Z0-9]+$/.test(address); } /** - * Checks if the string is a valid protocol public key or - * extended public key. - * - * @param {string} pub - the public key to be validated - * @returns {boolean} - the validation result + * Validates a block ID + * @param {string} hash - block ID to validate + * @returns {boolean} - validation result + */ + isValidBlockId(hash: string): boolean { + try { + const decoded = Buffer.from(hash, 'hex'); + return decoded.length === 32; + } catch { + return false; + } + } + + /** + * Validates a public key + * @param {string} pub - public key to validate + * @returns {boolean} - validation result */ isValidPublicKey(pub: string): boolean { if (isValidXpub(pub)) return true; let pubBuf: Buffer; - if (pub.length === SHORT_PUB_KEY_LENGTH) { + if (pub.length === 50) { try { - pubBuf = this.cb58Decode(pub); + pubBuf = Buffer.from(pub, 'hex'); } catch { return false; } } else { - if (pub.length !== COMPRESSED_PUBLIC_KEY_LENGTH && pub.length !== UNCOMPRESSED_PUBLIC_KEY_LENGTH) { - return false; - } + if (pub.length !== 66 && pub.length !== 130) return false; const firstByte = pub.slice(0, 2); - - // uncompressed public key - if (pub.length === UNCOMPRESSED_PUBLIC_KEY_LENGTH && firstByte !== '04') { - return false; - } - - // compressed public key - if (pub.length === COMPRESSED_PUBLIC_KEY_LENGTH && firstByte !== '02' && firstByte !== '03') { - return false; - } - + if (pub.length === 130 && firstByte !== '04') return false; + if (pub.length === 66 && firstByte !== '02' && firstByte !== '03') return false; if (!this.allHexChars(pub)) return false; + pubBuf = Buffer.from(pub, 'hex'); } - // validate the public key using BitGo secp256k1 + try { - ecc.isPoint(pubBuf); // Check if it's a valid point + ecc.isPoint(pubBuf); return true; } catch (e) { return false; } } - public parseAddress = (address: string): Buffer => { - return this.stringToAddress(address); - }; - - public stringToAddress = (address: string, hrp?: string): Buffer => { - const parts = address.trim().split('-'); - if (parts.length < 2) { - throw new Error('Error - Valid address should include -'); - } - - const split = parts[1].lastIndexOf('1'); - if (split < 0) { - throw new Error('Error - Valid address must include separator (1)'); - } - - const humanReadablePart = parts[1].slice(0, split); - if (humanReadablePart !== 'flare' && humanReadablePart !== 'costwo') { - throw new Error('Error - Invalid HRP'); - } - - return Buffer.from(bech32.fromWords(bech32.decode(parts[1]).words)); - }; - /** - * Returns whether or not the string is a valid protocol private key, or extended - * private key. - * - * The protocol key format is described in the @stacks/transactions npm package, in the - * createStacksPrivateKey function: - * https://github.com/blockstack/stacks.js/blob/master/packages/transactions/src/keys.ts#L125 - * - * @param {string} prv - the private key (or extended private key) to be validated - * @returns {boolean} - the validation result + * Validates a private key + * @param {string} prv - private key to validate + * @returns {boolean} - validation result */ isValidPrivateKey(prv: string): boolean { if (isValidXprv(prv)) return true; - - if (prv.length !== RAW_PRIVATE_KEY_LENGTH && prv.length !== SUFFIXED_PRIVATE_KEY_LENGTH) { - return false; - } - - if ( - prv.length === SUFFIXED_PRIVATE_KEY_LENGTH && - prv.slice(RAW_PRIVATE_KEY_LENGTH) !== PRIVATE_KEY_COMPRESSED_SUFFIX - ) { - return false; - } - + if (prv.length !== 64 && prv.length !== 66) return false; + if (prv.length === 66 && prv.slice(64) !== '01') return false; return this.allHexChars(prv); } /** - * Returns whether or not the string is a composed of hex chars only - * - * @param {string} maybe - the string to be validated - * @returns {boolean} - the validation result - */ - allHexChars(maybe: string): boolean { - return HEX_REGEX.test(maybe); - } - - /** - * Lightweight Ethereum address validation - * Validates that an address is a 40-character hex string (optionally prefixed with 0x) - * - * @param {string} address - the Ethereum address to validate - * @returns {boolean} - true if valid Ethereum address format - */ - isValidEthereumAddress(address: string): boolean { - if (!address || typeof address !== STRING_TYPE) { - return false; - } - - // Remove 0x prefix if present - const cleanAddress = address.startsWith('0x') ? address.slice(2) : address; - - // Check if it's exactly 40 hex characters - return cleanAddress.length === 40 && /^[0-9a-fA-F]{40}$/.test(cleanAddress); - } - - /** - * Pick specific properties from an object (replaces lodash.pick) - * - * @param {T} obj - the source object - * @param {K[]} keys - array of property keys to pick - * @returns {Pick} - new object with only the specified properties + * Checks if a string contains only hex characters */ - pick(obj: T, keys: K[]): Pick { - const result = {} as Pick; - for (const key of keys) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - result[key] = obj[key]; - } - } - return result; + allHexChars(str: string): boolean { + return /^(0x){0,1}([0-9a-f])+$/i.test(str); } /** - * Deep equality comparison (replaces lodash.isEqual) - * - * @param {unknown} a - first value to compare - * @param {unknown} b - second value to compare - * @returns {boolean} - true if values are deeply equal - */ - isEqual(a: unknown, b: unknown): boolean { - if (a === b) return true; - - if (a === null || a === undefined || b === null || b === undefined) return a === b; - - if (typeof a !== typeof b) return false; - - if (typeof a === 'object') { - if (Array.isArray(a) !== Array.isArray(b)) return false; - - if (Array.isArray(a)) { - const arrB = b as unknown[]; - if (a.length !== arrB.length) return false; - for (let i = 0; i < a.length; i++) { - if (!this.isEqual(a[i], arrB[i])) return false; - } - return true; - } - - const objA = a as Record; - const objB = b as Record; - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - if (!keysB.includes(key)) return false; - if (!this.isEqual(objA[key], objB[key])) return false; - } - return true; - } - - return false; - } - - /** @inheritdoc */ - isValidSignature(signature: string): boolean { - throw new NotImplementedError('isValidSignature not implemented'); - } - - /** @inheritdoc */ - isValidTransactionId(txId: string): boolean { - return this.isValidId(txId); - } - - /** @inheritdoc */ - isValidBlockId(blockId: string): boolean { - return this.isValidId(blockId); - } - - /** - * FlareJS wrapper to create signature and return it for credentials - * @param network - * @param message - * @param prv - * @return signature + * Creates a signature using the Flare network parameters */ createSignature(network: FlareNetwork, message: Buffer, prv: Buffer): Buffer { - // Used BitGo secp256k1 since FlareJS may not expose KeyPair in the same way - try { - // Hash the message first: secp256k1 signing requires a 32-byte hash as input. - // It is essential that the same hashing (sha256 of the message) is applied during signature recovery, - // otherwise the recovered public key or signature verification will fail. - const messageHash = createHash('sha256').update(message).digest(); - - // Sign with recovery parameter - const signature = ecc.sign(messageHash, prv); - - // Get recovery parameter by trying both values - let recoveryParam = -1; - const pubKey = ecc.pointFromScalar(prv, true); - if (!pubKey) { - throw new Error('Failed to derive public key from private key'); - } - const recovered0 = ecc.recoverPublicKey(messageHash, signature, 0, true); - if (recovered0 && Buffer.from(recovered0).equals(Buffer.from(pubKey))) { - recoveryParam = 0; - } else { - const recovered1 = ecc.recoverPublicKey(messageHash, signature, 1, true); - if (recovered1 && Buffer.from(recovered1).equals(Buffer.from(pubKey))) { - recoveryParam = 1; - } else { - throw new Error('Could not determine correct recovery parameter for signature'); - } - } - - // Append recovery parameter to signature - const fullSig = Buffer.alloc(65); // 64 bytes signature + 1 byte recovery - fullSig.set(signature); - fullSig[64] = recoveryParam; - - return fullSig; - } catch (error) { - throw new Error(`Failed to create signature: ${error}`); - } + const messageHash = this.sha256(message); + const signature = ecc.sign(messageHash, prv); + return Buffer.from(signature); } /** - * FlareJS wrapper to verify signature - * @param network - * @param message - * @param signature - * @param publicKey - public key instead of private key for verification - * @return true if it's verify successful + * Verifies a signature */ verifySignature(network: FlareNetwork, message: Buffer, signature: Buffer, publicKey: Buffer): boolean { try { - // Hash the message first - must match the hash used in signing - const messageHash = createHash('sha256').update(message).digest(); - - // Extract the actual signature without recovery parameter - if (signature.length !== 65) { - throw new Error('Invalid signature length - expected 65 bytes (64 bytes signature + 1 byte recovery)'); - } - const sigOnly = signature.slice(0, 64); - - return ecc.verify(messageHash, publicKey, sigOnly); - } catch (error) { + const messageHash = this.sha256(message); + return ecc.verify(signature, messageHash, publicKey); + } catch (e) { return false; } } /** - * FlareJS wrapper to recover signature - * @param network - * @param message - * @param signature - * @return recovered public key + * Creates a new signature object */ - recoverySignature(network: FlareNetwork, message: Buffer, signature: Buffer): Buffer { - try { - // Hash the message first - must match the hash used in signing - const messageHash = createHash('sha256').update(message).digest(); - - // Extract recovery parameter and signature - if (signature.length !== 65) { - throw new Error('Invalid signature length - expected 65 bytes (64 bytes signature + 1 byte recovery)'); - } - - const recoveryParam = signature[64]; - const sigOnly = signature.slice(0, 64); - - // Recover public key using the provided recovery parameter - const recovered = ecc.recoverPublicKey(messageHash, sigOnly, recoveryParam, true); - if (!recovered) { - throw new Error('Failed to recover public key'); - } - - return Buffer.from(recovered); - } catch (error) { - throw new Error(`Failed to recover signature: ${error}`); - } + createNewSig(sigHex: string): Signature { + const buffer = Buffer.from(sigHex.padStart(130, '0'), 'hex'); + return new Signature(buffer); } + /** + * Computes SHA256 hash + */ sha256(buf: Uint8Array): Buffer { return createHash('sha256').update(buf).digest(); } /** - * Check the raw transaction has a valid format in the blockchain context, throw otherwise. - * It's to reuse in TransactionBuilder and TransactionBuilderFactory - * - * @param rawTransaction Transaction as hex string + * Validates raw transaction format */ validateRawTransaction(rawTransaction: string): void { if (!rawTransaction) { throw new InvalidTransactionError('Raw transaction is empty'); } - if (!utils.allHexChars(rawTransaction)) { + if (!this.allHexChars(rawTransaction)) { throw new ParseTransactionError('Raw transaction is not hex string'); } } /** - * Check if tx is for the blockchainId - * - * @param {DeprecatedTx} tx - * @param {string} blockchainId - * @returns true if tx is for blockchainId - */ - isTransactionOf(tx: DeprecatedTx, blockchainId: string): boolean { - // FlareJS equivalent - this would need proper CB58 encoding implementation - try { - const txRecord = tx as unknown as Record; - const unsignedTx = (txRecord.getUnsignedTx as () => Record)(); - const transaction = (unsignedTx.getTransaction as () => Record)(); - const txBlockchainId = (transaction.getBlockchainID as () => unknown)(); - return Buffer.from(txBlockchainId as string).toString(HEX_ENCODING) === blockchainId; - } catch (error) { - return false; - } - } - - /** - * Check if Output is from PVM. - * Output could be EVM or PVM output. - * @param {DeprecatedOutput} output - * @returns {boolean} output has transferable output structure - */ - deprecatedIsTransferableOutput(output: DeprecatedOutput): boolean { - return 'getOutput' in (output as Record); - } - - /** - * Check if Output is from PVM. - * Output could be EVM or PVM output. - * @param {Output} output - * @returns {boolean} output is TransferableOutput + * Checks if output is TransferableOutput type */ isTransferableOutput(output: Output): output is TransferableOutput { - return typeof (output as unknown as Record).getOutput === 'function'; - } - - /** - * Return a mapper function to that network address representation. - * @param network required to stringify addresses - * @return mapper function - */ - deprecatedMapOutputToEntry(network: FlareNetwork): (output: DeprecatedOutput) => Entry { - return (output: DeprecatedOutput) => { - if (this.deprecatedIsTransferableOutput(output)) { - // Simplified implementation for FlareJS - try { - const transferableOutput = output as unknown as TransferableOutput; - const amount = transferableOutput.amount(); - - // Simplified address handling - would need proper FlareJS address utilities - const address = FLARE_ADDRESS_PLACEHOLDER; // TODO: implement proper address conversion - - return { - value: amount.toString(), - address, - }; - } catch (error) { - throw new Error(`Failed to map output: ${error}`); - } - } else { - // Handle EVM output case - simplified - return { - value: '0', // TODO: implement proper amount extraction - address: '0x0000000000000000000000000000000000000000', // TODO: implement proper address extraction - }; - } - }; + return output?._type === TypeSymbols.TransferableOutput; } /** - * Return a mapper function to that network address representation. - * @param network required to stringify addresses - * @return mapper function + * Maps outputs to entry format */ mapOutputToEntry(network: FlareNetwork): (Output) => Entry { return (output: Output) => { if (this.isTransferableOutput(output)) { - const transferableOutput = output as TransferableOutput; - const outputAmount = transferableOutput.amount(); - - // Simplified address handling for FlareJS - const address = 'flare-address-placeholder'; // TODO: implement proper address conversion - + const outputAmount = output.amount(); + const address = (output.output as TransferOutput) + .getOwners() + .map((a) => this.addressToString(network.hrp, network.alias, Buffer.from(a))) + .sort() + .join(ADDRESS_SEPARATOR); return { value: outputAmount.toString(), address, @@ -501,107 +194,54 @@ export class Utils implements BaseUtils { } /** - * remove hex prefix (0x) - * @param hex string - * @returns hex without 0x + * Removes 0x prefix from hex string */ removeHexPrefix(hex: string): string { - if (hex.startsWith('0x')) { - return hex.substring(2); - } - return hex; + return hex.startsWith('0x') ? hex.substring(2) : hex; } /** - * Outputidx convert from number (as string) to buffer. - * @param {string} outputidx number - * @return {Buffer} buffer of size 4 with that number value + * Converts output index to buffer */ outputidxNumberToBuffer(outputidx: string): Buffer { - return Buffer.from( - Number(outputidx).toString(HEX_RADIX).padStart(OUTPUT_INDEX_HEX_LENGTH, PADSTART_CHAR), - HEX_ENCODING - ); + return Buffer.from(Number(outputidx).toString(16).padStart(8, '0'), 'hex'); } /** - * Outputidx buffer to number (as string) - * @param {Buffer} outputidx - * @return {string} outputidx number + * Converts output index buffer to number string */ outputidxBufferToNumber(outputidx: Buffer): string { - return parseInt(outputidx.toString(HEX_ENCODING), HEX_RADIX).toString(); + return parseInt(outputidx.toString('hex'), 16).toString(); } - /** - * Convert string to bytes for FlareJS memo - * Follows FlareJS utils.stringToBytes pattern - * @param {string} text - Text to convert - * @returns {Uint8Array} Byte array - */ - stringToBytes(text: string): Uint8Array { - return new TextEncoder().encode(text); + // Required by BaseUtils interface but not implemented + isValidSignature(signature: string): boolean { + throw new NotImplementedError('isValidSignature not implemented'); } - /** - * Convert bytes to string from FlareJS memo - * @param {Uint8Array} bytes - Bytes to convert - * @returns {string} Decoded string - */ - bytesToString(bytes: Uint8Array): string { - return new TextDecoder().decode(bytes); + isValidTransactionId(txId: string): boolean { + throw new NotImplementedError('isValidTransactionId not implemented'); } /** - * Create memo bytes from various input formats - * Supports string, JSON object, or raw bytes - * @param {string | Record | Uint8Array} memo - Memo data - * @returns {Uint8Array} Memo bytes for FlareJS + * Helper method to convert address components to string */ - createMemoBytes(memo: string | Record | Uint8Array): Uint8Array { - if (memo instanceof Uint8Array) { - return memo; - } - - if (typeof memo === STRING_TYPE) { - return this.stringToBytes(memo as string); - } - - if (typeof memo === 'object') { - return this.stringToBytes(JSON.stringify(memo)); - } - - throw new InvalidTransactionError('Invalid memo format'); - } + public addressToString = (hrp: string, prefix: string, address: Buffer): string => { + // Convert the address bytes to 5-bit words for bech32 encoding + const words = bech32.toWords(address); + // Create the full bech32 address with format: P-{hrp}1{bech32_encoded_address} + return `${prefix}-${bech32.encode(hrp, words)}`; + }; /** - * Parse memo bytes to string - * @param {Uint8Array} memoBytes - Memo bytes from FlareJS transaction - * @returns {string} Decoded memo string + * Decodes a base58 string with checksum to a Buffer */ - parseMemoBytes(memoBytes: Uint8Array): string { - if (memoBytes.length === 0) { - return ''; + public cb58Decode(str: string): Buffer { + const decoded = bs58.decode(str); + if (!this.validateChecksum(Buffer.from(decoded))) { + throw new Error('Invalid checksum'); } - return this.bytesToString(memoBytes); - } - - /** - * Validate memo size (FlareJS has transaction size limits) - * @param {Uint8Array} memoBytes - Memo bytes - * @param {number} maxSize - Maximum size in bytes (default 4KB) - * @returns {boolean} Whether memo is within size limits - */ - validateMemoSize(memoBytes: Uint8Array, maxSize = 4096): boolean { - return memoBytes.length <= maxSize; - } - - /** - * Adds a checksum to a Buffer and returns the concatenated result - */ - private addChecksum(buff: Buffer): Buffer { - const hashSlice = createHash('sha256').update(buff).digest().slice(28); - return Buffer.concat([buff, hashSlice]); + return Buffer.from(decoded.slice(0, decoded.length - 4)); } /** @@ -625,37 +265,80 @@ export class Utils implements BaseUtils { } /** - * Decodes a base58 string with checksum to a Buffer + * Adds a checksum to a Buffer and returns the concatenated result */ - public cb58Decode(str: string): Buffer { - const decoded = bs58.decode(str); - if (!this.validateChecksum(Buffer.from(decoded))) { - throw new Error('Invalid checksum'); - } - return Buffer.from(decoded.slice(0, decoded.length - 4)); + private addChecksum(buff: Buffer): Buffer { + const hashSlice = createHash('sha256').update(buff).digest().slice(28); + return Buffer.concat([buff, hashSlice]); } + // In utils.ts, add this method to the Utils class: + /** - * Checks if a string is a valid CB58 (base58 with checksum) format + * Parse an address string into a Buffer + * @param address - The address to parse + * @returns Buffer containing the parsed address */ - private isCB58(str: string): boolean { - try { - this.cb58Decode(str); - return true; - } catch { - return false; + //TODO: need check and validate this method + public parseAddress = (address: string): Buffer => { + return this.stringToAddress(address); + }; + + public stringToAddress = (address: string, hrp?: string): Buffer => { + // Handle hex addresses + if (address.startsWith('0x')) { + return Buffer.from(address.slice(2), 'hex'); } - } - isValidId(id: string): boolean { + // 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 -'); + } + + const split = parts[1].lastIndexOf('1'); + if (split < 0) { + throw new Error('Error - Valid bech32 address must include separator (1)'); + } + + const humanReadablePart = parts[1].slice(0, split); + if (humanReadablePart !== 'flare' && humanReadablePart !== 'costwo') { + throw new Error('Error - Invalid HRP'); + } + + return Buffer.from(bech32.fromWords(bech32.decode(parts[1]).words)); + }; + + /** + * Check if tx is for the blockchainId + * + * @param {DeprecatedTx} tx + * @param {string} blockchainId + * @returns true if tx is for blockchainId + */ + // TODO: remove DeprecatedTx usage + isTransactionOf(tx: DeprecatedTx, blockchainId: string): boolean { + // FlareJS equivalent - this would need proper CB58 encoding implementation try { - return this.isCB58(id) && this.cb58Decode(id).length === DECODED_BLOCK_ID_LENGTH; - } catch { + const txRecord = tx as unknown as Record; + const unsignedTx = (txRecord.getUnsignedTx as () => Record)(); + const transaction = (unsignedTx.getTransaction as () => Record)(); + const txBlockchainId = (transaction.getBlockchainID as () => unknown)(); + return Buffer.from(txBlockchainId as string).toString('hex') === blockchainId; + } catch (error) { return false; } } + + flareIdString(value: string): Id { + return new Id(Buffer.from(value, 'hex')); + } } const utils = new Utils(); - export default utils; diff --git a/modules/sdk-coin-flrp/src/lib/validatorTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/validatorTxBuilder.ts deleted file mode 100644 index 0c320afcb5..0000000000 --- a/modules/sdk-coin-flrp/src/lib/validatorTxBuilder.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { DelegatorTxBuilder } from './delegatorTxBuilder'; -import { Tx } from './iface'; -import { RawTransactionData, ValidatorExtendedTransaction, ValidatorRawTransactionData } from './types'; -import { - MIN_DELEGATION_FEE_BASIS_POINTS, - OBJECT_TYPE_STRING, - STRING_TYPE, - VALIDATOR_TRANSACTION_TYPES, -} from './constants'; - -export class ValidatorTxBuilder extends DelegatorTxBuilder { - protected _delegationFeeRate: number | undefined; - - /** - * @param coinConfig - */ - constructor(coinConfig: Readonly) { - super(coinConfig); - this._delegationFeeRate = undefined; - } - - /** - * get transaction type - * @protected - */ - protected get transactionType(): TransactionType { - return TransactionType.AddValidator; - } - - /** - * set the delegationFeeRate - * @param value number - */ - delegationFeeRate(value: number): this { - this.validateDelegationFeeRate(value); - this._delegationFeeRate = value; - return this; - } - - /** - * Validate that the delegation fee is at least the minDelegationFee - * @param delegationFeeRate number - */ - validateDelegationFeeRate(delegationFeeRate: number): void { - // For Flare, use a minimum delegation fee of 2% (20000 basis points) - const minDelegationFee = MIN_DELEGATION_FEE_BASIS_POINTS; // 2% - if (delegationFeeRate < minDelegationFee) { - throw new BuildTransactionError(`Delegation fee cannot be less than ${minDelegationFee} basis points (2%)`); - } - } - - /** @inheritdoc */ - initBuilder(tx: Tx): this { - super.initBuilder(tx); - - // Extract delegation fee rate from transaction if available - const txData = tx as unknown as RawTransactionData; - const validatorData = txData as ValidatorRawTransactionData; - if (validatorData.delegationFeeRate !== undefined) { - this._delegationFeeRate = validatorData.delegationFeeRate; - } - - return this; - } - - /** - * Verify if the transaction is an AddValidator transaction - * @param tx - */ - static verifyTxType(tx: unknown): boolean { - // FlareJS validator transaction type verification - try { - if (!tx || typeof tx !== OBJECT_TYPE_STRING) { - return false; - } - - const txData = tx as Record; - - // Check for validator transaction type markers - const validValidatorTypes = VALIDATOR_TRANSACTION_TYPES; - - // Primary type verification - if (txData.type && typeof txData.type === STRING_TYPE) { - if (validValidatorTypes.includes(txData.type as string)) { - return true; - } - } - - // Secondary verification through transaction structure - const hasValidatorStructure = - // Has delegation fee rate (unique to validators) - (typeof txData.delegationFeeRate === 'number' || - (txData.delegation && - typeof (txData.delegation as Record).delegationFeeRate === 'number')) && - // Has node ID - (txData.nodeID || (txData.validator && (txData.validator as Record).nodeID)) && - // Has staking information - (txData.stakeAmount || (txData.stake && (txData.stake as Record).amount)); - - // FlareJS-specific markers - const hasFlareJSValidatorMarkers = - txData._txType === 'addValidator' || - txData._validatorType === 'primary' || - (txData.validator && (txData.validator as Record)._flareJSReady === true); - - return Boolean(hasValidatorStructure || hasFlareJSValidatorMarkers); - } catch (error) { - return false; - } - } - - verifyTxType(tx: unknown): boolean { - return ValidatorTxBuilder.verifyTxType(tx); - } - - /** - * Build the validator transaction using FlareJS PVM API - * @protected - */ - protected async buildFlareTransaction(): Promise { - // Basic validation - if (!this._nodeID) { - throw new BuildTransactionError('Node ID is required for validator transaction'); - } - if (!this._startTime) { - throw new BuildTransactionError('Start time is required for validator transaction'); - } - if (!this._endTime) { - throw new BuildTransactionError('End time is required for validator transaction'); - } - if (!this._stakeAmount) { - throw new BuildTransactionError('Stake amount is required for validator transaction'); - } - if (this._delegationFeeRate === undefined) { - throw new BuildTransactionError('Delegation fee rate is required for validator transaction'); - } - - const rewardAddresses = (this.transaction as unknown as ValidatorExtendedTransaction)._rewardAddresses; - if (!rewardAddresses || rewardAddresses.length === 0) { - throw new BuildTransactionError('Reward addresses are required for validator transaction'); - } - - // Validate time range (inherited from DelegatorTxBuilder logic) - if (this._endTime <= this._startTime) { - throw new BuildTransactionError('End time must be after start time'); - } - - try { - // FlareJS PVM API implementation for validator transactions - // This creates a structured validator transaction compatible with FlareJS patterns - - const enhancedValidatorTx = { - type: 'PlatformVM.AddValidatorTx', - networkID: this.transaction._networkID, - blockchainID: this.transaction._blockchainID, - - // Enhanced validator information structure - validator: { - nodeID: this._nodeID, - startTime: this._startTime, - endTime: this._endTime, - stakeAmount: this._stakeAmount, - // FlareJS validator markers - _validatorType: 'primary', - _flareJSReady: true, - _pvmCompatible: true, - }, - - // Enhanced delegation information - delegation: { - delegationFeeRate: this._delegationFeeRate, - rewardAddress: rewardAddresses[0], - // FlareJS delegation markers - _delegationType: 'validator', - _feeRate: this._delegationFeeRate, - _flareJSReady: true, - }, - - // Enhanced transaction metadata - stake: { - assetID: this.getAssetId(), - amount: this._stakeAmount, - addresses: this.transaction._fromAddresses, - threshold: this.transaction._threshold || 1, - locktime: this.transaction._locktime || 0n, - // FlareJS stake markers - _stakeType: 'validator', - _flareJSReady: true, - }, - - // Enhanced credential structure for validators - credentials: this.transaction._fromAddresses.map(() => ({ - signatures: [], // Will be populated by FlareJS signing - _credentialType: 'secp256k1fx.Credential', - _validatorCredential: true, - _flareJSReady: true, - })), - - // Transaction metadata - memo: Buffer.alloc(0), - }; - - this.transaction.setTransaction(enhancedValidatorTx); - } catch (error) { - throw new BuildTransactionError( - `Failed to build validator transaction: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - } -} 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..687afd4c3d --- /dev/null +++ b/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts @@ -0,0 +1,29 @@ +// Test data for building export transactions with multiple P-addresses +export const EXPORT_IN_C = { + txhash: 'jHRxuZjnSHYNwWpUUyob7RpfHwj1wfuQa8DGWQrkDh2RQ5Jb3', + unsignedHex: + '0x0000000000010000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000017dae940e7fbd1854207be51da222ec43f93b7d0b000000000098968000000000000000000000000000000000000000000000000000000000000000000000000000000009000000010000000000000000000000000000000000000000000000000000000000000000000000070000000000895427000000000000000a000000020000000103e1085f6e146def5a2c7bac91be5aab59710bbd0000000100000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000215afad8', + signedHex: + '0x0000000000010000007278db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000000000000000000000000000000000000000000000000000000000000000000017dae940e7fbd1854207be51da222ec43f93b7d0b000000000098968000000000000000000000000000000000000000000000000000000000000000000000000000000009000000010000000000000000000000000000000000000000000000000000000000000000000000070000000000895427000000000000000a000000020000000103e1085f6e146def5a2c7bac91be5aab59710bbd0000000100000009000000018ef6432440c6a0b91c7cecef787ca94742abd0759ecd98e3864992182d6b05eb6f39f08b1e032b051ff5f7893b752338bd7614a9d0b462d68bfb863680382c6c01e44d605a', + xPrivateKey: + 'xprv9s21ZrQH143K2DW9jvDoAkVpRKi5V9XhZaVdoUcqoYPPQ9wRrLNT6VGgWBbRoSYB39Lak6kXgdTM9T3QokEi5n2JJ8EdggHLkZPX8eDiBu1', + signature: [ + '0x8ef6432440c6a0b91c7cecef787ca94742abd0759ecd98e3864992182d6b05eb6f39f08b1e032b051ff5f7893b752338bd7614a9d0b462d68bfb863680382c6c01', + ], + privateKey: 'bac20595af556338287cb631060473364b023dca089c50f87efd18e70655574d', + publicKey: '028fe87afe7b6a6a7f51beaf95357cb5a3cd75da16f8b24fa866d6ab8aef0dcabc', + amount: '8999975', + cHexAddress: '0x7Dae940e7fBd1854207Be51dA222Ec43f93b7d0b', + pAddresses: [ + 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + 'P-costwo1n4a86kc3td6nvmwm4xh0w78mc5jjxc9g8w6en0', + 'P-costwo1nhm2vw8653f3qwtj3kl6qa359kkt6y9r7qgljv', + ], + mainAddress: 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + pAddressRelatedToPrivateKey: 'P-costwo1kr3937ujjftcue445uxfesz7w6247pf2a8ts4k', + corethAddress: 'C-costwo1kr3937ujjftcue445uxfesz7w6247pf2a8ts4k', + targetChainId: '11111111111111111111111111111111LpoYY', + nonce: 9, + threshold: 2, + fee: '25', +}; diff --git a/modules/sdk-coin-flrp/test/unit/delegatorTxBuilder.test.ts b/modules/sdk-coin-flrp/test/unit/delegatorTxBuilder.test.ts deleted file mode 100644 index 0810566550..0000000000 --- a/modules/sdk-coin-flrp/test/unit/delegatorTxBuilder.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { DelegatorTxBuilder } from '../../src/lib/delegatorTxBuilder'; - -describe('DelegatorTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: DelegatorTxBuilder; - - beforeEach(function () { - builder = new DelegatorTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof DelegatorTxBuilder); - }); - - it('should initialize with default values', function () { - assert.strictEqual(builder['_nodeID'], ''); - assert.strictEqual(builder['_startTime'], 0n); - assert.strictEqual(builder['_endTime'], 0n); - assert.strictEqual(builder['_stakeAmount'], 0n); - }); - - it('should set transaction type to AddDelegator', function () { - assert.strictEqual(builder['transactionType'], TransactionType.AddDelegator); - }); - }); - - describe('nodeID', function () { - it('should set node ID correctly', function () { - const nodeID = 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'; - const result = builder.nodeID(nodeID); - assert.strictEqual(builder['_nodeID'], nodeID); - assert.strictEqual(result, builder); - }); - - it('should throw error for empty node ID', function () { - assert.throws(() => builder.nodeID(''), BuildTransactionError); - }); - }); - - describe('startTime', function () { - it('should set start time from number', function () { - const time = 1640995200; - const result = builder.startTime(time); - assert.strictEqual(builder['_startTime'], BigInt(time)); - assert.strictEqual(result, builder); - }); - - it('should set start time from string', function () { - const time = '1640995200'; - const result = builder.startTime(time); - assert.strictEqual(builder['_startTime'], BigInt(time)); - assert.strictEqual(result, builder); - }); - - it('should set start time from bigint', function () { - const time = 1640995200n; - const result = builder.startTime(time); - assert.strictEqual(builder['_startTime'], time); - assert.strictEqual(result, builder); - }); - - it('should throw error for zero start time', function () { - assert.throws(() => builder.startTime(0), BuildTransactionError); - }); - - it('should throw error for negative start time', function () { - assert.throws(() => builder.startTime(-1), BuildTransactionError); - }); - - it('should throw error when start time is after end time', function () { - builder.endTime(1640995200); // Set end time first - assert.throws(() => builder.startTime(1672531200), BuildTransactionError); // Start time after end time - }); - - it('should allow start time before end time', function () { - builder.endTime(1672531200); // Set end time first - const result = builder.startTime(1640995200); // Start time before end time - assert.strictEqual(result, builder); - }); - }); - - describe('endTime', function () { - it('should set end time from number', function () { - const time = 1672531200; - const result = builder.endTime(time); - assert.strictEqual(builder['_endTime'], BigInt(time)); - assert.strictEqual(result, builder); - }); - - it('should set end time from string', function () { - const time = '1672531200'; - const result = builder.endTime(time); - assert.strictEqual(builder['_endTime'], BigInt(time)); - assert.strictEqual(result, builder); - }); - - it('should set end time from bigint', function () { - const time = 1672531200n; - const result = builder.endTime(time); - assert.strictEqual(builder['_endTime'], time); - assert.strictEqual(result, builder); - }); - - it('should throw error for zero end time', function () { - assert.throws(() => builder.endTime(0), BuildTransactionError); - }); - - it('should throw error for negative end time', function () { - assert.throws(() => builder.endTime(-1), BuildTransactionError); - }); - - it('should throw error when end time is before start time', function () { - builder.startTime(1672531200); // Set start time first - assert.throws(() => builder.endTime(1640995200), BuildTransactionError); // End time before start time - }); - - it('should allow end time after start time', function () { - builder.startTime(1640995200); // Set start time first - const result = builder.endTime(1672531200); // End time after start time - assert.strictEqual(result, builder); - }); - }); - - describe('stakeAmount', function () { - it('should set stake amount from number', function () { - const amount = 50000000000000000000n; - const result = builder.stakeAmount(amount); - assert.strictEqual(builder['_stakeAmount'], amount); - assert.strictEqual(result, builder); - }); - - it('should set stake amount from string', function () { - const amount = '50000000000000000000'; - const result = builder.stakeAmount(amount); - assert.strictEqual(builder['_stakeAmount'], BigInt(amount)); - assert.strictEqual(result, builder); - }); - - it('should throw error for zero stake amount', function () { - assert.throws(() => builder.stakeAmount(0), BuildTransactionError); - }); - - it('should throw error for negative stake amount', function () { - assert.throws(() => builder.stakeAmount(-1), BuildTransactionError); - }); - }); - - describe('rewardAddresses', function () { - it('should set reward addresses correctly', function () { - const addresses = ['P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpk7kph']; - const result = builder.rewardAddresses(addresses); - assert.strictEqual(result, builder); - }); - - it('should throw error for empty addresses array', function () { - assert.throws(() => builder.rewardAddresses([]), BuildTransactionError); - }); - }); - - describe('verifyTxType', function () { - it('should return true for valid delegator transaction', function () { - const tx = { - nodeID: 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg', - stakeAmount: '50000000000000000000', - }; - assert.strictEqual(DelegatorTxBuilder.verifyTxType(tx), true); - assert.strictEqual(builder.verifyTxType(tx), true); - }); - - it('should return false for invalid transaction', function () { - const tx = { stakeAmount: '50000000000000000000' }; - assert.strictEqual(DelegatorTxBuilder.verifyTxType(tx), false); - }); - - it('should return false for empty transaction', function () { - assert.strictEqual(DelegatorTxBuilder.verifyTxType({}), false); - }); - }); - - describe('buildFlareTransaction', function () { - beforeEach(function () { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1672531200) - .stakeAmount(50000000000000000000n) - .rewardAddresses(['P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpk7kph']); - }); - - it('should throw error if nodeID is missing', async function () { - builder['_nodeID'] = ''; - await assert.rejects(builder['buildFlareTransaction'](), BuildTransactionError); - }); - - it('should throw error if startTime is missing', async function () { - builder['_startTime'] = 0n; - await assert.rejects(builder['buildFlareTransaction'](), BuildTransactionError); - }); - - it('should throw error if endTime is missing', async function () { - builder['_endTime'] = 0n; - await assert.rejects(builder['buildFlareTransaction'](), BuildTransactionError); - }); - - it('should throw error if stakeAmount is missing', async function () { - builder['_stakeAmount'] = 0n; - await assert.rejects(builder['buildFlareTransaction'](), BuildTransactionError); - }); - - it('should throw error if end time is before start time', function () { - // This test verifies that time validation happens immediately when setting values - assert.throws(() => { - builder.startTime(1672531200).endTime(1640995200); - }, BuildTransactionError); - }); - }); - - describe('method chaining', function () { - it('should support method chaining', function () { - const result = builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1672531200) - .stakeAmount(50000000000000000000n) - .rewardAddresses(['P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpk7kph']); - - assert.strictEqual(result, builder); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/flrp.ts b/modules/sdk-coin-flrp/test/unit/flrp.ts deleted file mode 100644 index e446c3dda4..0000000000 --- a/modules/sdk-coin-flrp/test/unit/flrp.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { coins } from '@bitgo/statics'; -import * as assert from 'assert'; -import { Flrp } from '../../src/flrp'; -import { BitGoBase } from '@bitgo/sdk-core'; -import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; -import { BitGoAPI } from '@bitgo/sdk-api'; - -describe('Flrp', function () { - let bitgo: TestBitGoAPI; - const staticsCoin = coins.get('flrp'); - - before(function () { - bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); - bitgo.initializeTestVars(); - // Attempt to register the coin symbol; safeRegister is idempotent. - (bitgo as unknown as { safeRegister?: (n: string, f: (bg: BitGoBase) => unknown) => void }).safeRegister?.( - 'flrp', - Flrp.createInstance - ); - }); - - describe('createInstance', function () { - it('should return a Flrp instance', function () { - const coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin); - assert.ok(coin instanceof Flrp); - }); - - it('should produce distinct objects on multiple calls', function () { - const a = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin); - const b = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin); - assert.notStrictEqual(a, b); - assert.ok(a instanceof Flrp); - assert.ok(b instanceof Flrp); - }); - }); - - describe('coin properties', function () { - let coin: Flrp; - - beforeEach(function () { - coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin) as Flrp; - }); - - it('should have correct coin family', function () { - assert.strictEqual(coin.getFamily(), staticsCoin.family); - }); - - it('should have correct coin name', function () { - assert.strictEqual(coin.getFullName(), staticsCoin.fullName); - }); - - it('should have correct base factor', function () { - assert.strictEqual(coin.getBaseFactor(), Math.pow(10, staticsCoin.decimalPlaces)); - }); - - it('should validate addresses using utils', function () { - const validAddress = 'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh'; - const result = coin.isValidAddress(validAddress); - assert.strictEqual(typeof result, 'boolean'); - }); - - it('should generate key pairs', function () { - const keyPair = coin.generateKeyPair(); - assert.ok('pub' in keyPair); - assert.ok('prv' in keyPair); - if (keyPair.pub && keyPair.prv) { - assert.strictEqual(typeof keyPair.pub, 'string'); - assert.strictEqual(typeof keyPair.prv, 'string'); - } - }); - }); - - describe('error handling', function () { - it('should handle construction with invalid parameters', function () { - assert.throws(() => Flrp.createInstance(null as unknown as BitGoBase, staticsCoin)); - assert.throws(() => Flrp.createInstance(bitgo as unknown as BitGoBase, null as unknown as typeof staticsCoin)); - }); - }); - - describe('inheritance and methods', function () { - let coin: Flrp; - - beforeEach(function () { - coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin) as Flrp; - }); - - it('should have required base coin methods', function () { - assert.ok('getFamily' in coin); - assert.ok('getFullName' in coin); - assert.ok('getBaseFactor' in coin); - assert.ok('isValidAddress' in coin); - assert.ok('generateKeyPair' in coin); - }); - - it('should handle address validation consistently', function () { - const validAddress = 'flare1test'; - const invalidAddress = 'invalid-address'; - - assert.strictEqual(typeof coin.isValidAddress(validAddress), 'boolean'); - assert.strictEqual(typeof coin.isValidAddress(invalidAddress), 'boolean'); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/atomicTransactionBuilder.ts deleted file mode 100644 index 07c4be1a10..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/atomicTransactionBuilder.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { Credential, Signature } from '@flarenetwork/flarejs'; -import { AtomicTransactionBuilder } from '../../../src/lib/atomicTransactionBuilder'; - -// Concrete implementation for testing -class TestAtomicTransactionBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Send; - } - - // Expose protected method for testing - public testCreateFlareCredential(credentialId: number, signatures: string[]): Credential { - return this.createFlareCredential(credentialId, signatures); - } - - // Expose protected method for testing - public testValidateCredentials(credentials: Credential[]): void { - return this.validateCredentials(credentials); - } - - // Expose protected method for testing - public testCreateInputOutput(total: bigint) { - return this.createInputOutput(total); - } -} - -describe('AtomicTransactionBuilder', function () { - let builder: TestAtomicTransactionBuilder; - const coinConfig = coins.get('flrp'); - - beforeEach(function () { - builder = new TestAtomicTransactionBuilder(coinConfig); - }); - - describe('constructor', function () { - it('should create instance with proper coin config', function () { - assert.ok(builder instanceof AtomicTransactionBuilder); - assert.strictEqual(builder['_coinConfig'], coinConfig); - }); - - it('should initialize transaction state properly', function () { - const transaction = builder['transaction']; - assert.strictEqual(typeof transaction._network, 'object'); - assert.strictEqual(transaction._networkID, 0); - assert.ok(Array.isArray(transaction._fromAddresses)); - assert.strictEqual(transaction._fromAddresses.length, 0); - assert.ok(Array.isArray(transaction._to)); - assert.strictEqual(transaction._to.length, 0); - assert.strictEqual(transaction._locktime, 0n); - assert.strictEqual(transaction._threshold, 1); - assert.strictEqual(transaction._fee.fee, '0'); - assert.strictEqual(transaction.hasCredentials, false); - }); - }); - - describe('validateAmount', function () { - it('should accept positive amounts', function () { - assert.doesNotThrow(() => builder.validateAmount(1n)); - assert.doesNotThrow(() => builder.validateAmount(100n)); - assert.doesNotThrow(() => builder.validateAmount(BigInt('1000000000000000000'))); - }); - - it('should reject zero amount', function () { - assert.throws(() => builder.validateAmount(0n), BuildTransactionError, 'Amount must be positive'); - }); - - it('should reject negative amounts', function () { - assert.throws(() => builder.validateAmount(-1n), BuildTransactionError, 'Amount must be positive'); - assert.throws(() => builder.validateAmount(-100n), BuildTransactionError, 'Amount must be positive'); - }); - }); - - describe('createFlareCredential', function () { - const validHexSignature = '3045022100' + '0'.repeat(56) + '02200' + '1'.repeat(55); - - it('should create credential with valid signatures', function () { - const signatures = [validHexSignature, '']; - const credential = builder.testCreateFlareCredential(0, signatures); - - assert.ok(credential instanceof Credential); - const sigArray = credential.getSignatures(); - assert.strictEqual(sigArray.length, 2); - }); - - it('should handle empty signatures as placeholders', function () { - const signatures = ['', '']; - const credential = builder.testCreateFlareCredential(0, signatures); - - assert.ok(credential instanceof Credential); - const sigArray = credential.getSignatures(); - assert.strictEqual(sigArray.length, 2); - }); - - it('should handle hex signatures with 0x prefix', function () { - const signatures = [`0x${validHexSignature}`]; - const credential = builder.testCreateFlareCredential(0, signatures); - - assert.ok(credential instanceof Credential); - const sigArray = credential.getSignatures(); - assert.strictEqual(sigArray.length, 1); - }); - - it('should throw error for non-array signatures', function () { - assert.throws( - () => builder.testCreateFlareCredential(0, 'invalid' as unknown as string[]), - BuildTransactionError, - 'Signatures must be an array' - ); - }); - - it('should throw error for empty signatures array', function () { - assert.throws( - () => builder.testCreateFlareCredential(0, []), - BuildTransactionError, - 'Signatures array cannot be empty' - ); - }); - - it('should throw error for invalid hex characters', function () { - const invalidSig = '304502210xyz'; // Contains invalid hex chars - assert.throws( - () => builder.testCreateFlareCredential(0, [invalidSig]), - BuildTransactionError, - 'Invalid hex signature at index 0: contains non-hex characters' - ); - }); - - it('should throw error for signatures that are too long', function () { - const longSig = 'a'.repeat(200); // 100 bytes, longer than 65 - assert.throws(() => builder.testCreateFlareCredential(0, [longSig]), BuildTransactionError); - }); - - it('should handle signatures shorter than 65 bytes', function () { - const shortSig = 'abcd1234'; // 4 bytes - const credential = builder.testCreateFlareCredential(0, [shortSig]); - - assert.ok(credential instanceof Credential); - }); - }); - - describe('validateCredentials', function () { - it('should accept valid credentials array', function () { - const credential = new Credential([new Signature(new Uint8Array(65))]); - assert.doesNotThrow(() => builder.testValidateCredentials([credential])); - }); - - it('should accept empty credentials array', function () { - assert.doesNotThrow(() => builder.testValidateCredentials([])); - }); - - it('should throw error for non-array input', function () { - assert.throws( - () => builder.testValidateCredentials('invalid' as unknown as Credential[]), - BuildTransactionError, - 'Credentials must be an array' - ); - }); - - it('should throw error for invalid credential objects', function () { - const invalidCredentials = [{ fake: 'credential' }] as unknown as Credential[]; - assert.throws( - () => builder.testValidateCredentials(invalidCredentials), - BuildTransactionError, - 'Invalid credential at index 0' - ); - }); - - it('should throw error for mixed valid/invalid credentials', function () { - const validCredential = new Credential([new Signature(new Uint8Array(65))]); - const invalidCredential = { fake: 'credential' }; - const credentials = [validCredential, invalidCredential] as unknown as Credential[]; - - assert.throws( - () => builder.testValidateCredentials(credentials), - BuildTransactionError, - 'Invalid credential at index 1' - ); - }); - }); - - describe('createInputOutput', function () { - const sampleUtxos = [ - { - outputID: 7, - amount: '1000000', - txid: '1234567890abcdef1234567890abcdef12345678', - outputidx: '0', - threshold: 2, - addresses: ['P-test1234567890abcdef', 'P-test567890abcdef1234'], - }, - { - outputID: 7, - amount: '500000', - txid: 'abcdef1234567890abcdef1234567890abcdef12', - outputidx: '1', - threshold: 2, - addresses: ['P-test1234567890abcdef', 'P-test567890abcdef1234'], - }, - ]; - - it('should return empty structure when no UTXOs set', function () { - assert.throws(() => builder.testCreateInputOutput(100n), /UTXOs are required for creating inputs and outputs/); - }); - - it('should process UTXOs and return structured output', function () { - // Set UTXOs first - builder.utxos(sampleUtxos); - - const result = builder.testCreateInputOutput(100000n); - - assert.ok('inputs' in result); - assert.ok('outputs' in result); - assert.ok('credentials' in result); - - assert.ok(Array.isArray(result.inputs)); - assert.ok(Array.isArray(result.outputs)); - assert.ok(Array.isArray(result.credentials)); - assert.strictEqual(result.credentials.length, 1); // Should create credential for first UTXO - }); - - it('should handle insufficient funds', function () { - builder.utxos(sampleUtxos); - - // Request more than available (total available is 1,500,000) - assert.throws(() => builder.testCreateInputOutput(2000000n), /Insufficient funds: need 2000000, have 1500000/); - }); - - it('should use multiple UTXOs when needed', function () { - builder.utxos(sampleUtxos); - - // Request amount that requires both UTXOs - const result = builder.testCreateInputOutput(1200000n); - - assert.strictEqual(result.credentials.length, 2); // Should use both UTXOs - }); - }); - - describe('initBuilder', function () { - it('should return this for fluent API', function () { - const result = builder.initBuilder({}); - assert.strictEqual(result, builder); - }); - - it('should handle different transaction objects', function () { - const tx1 = { id: '123' }; - const tx2 = { data: 'test' }; - - assert.strictEqual(builder.initBuilder(tx1), builder); - assert.strictEqual(builder.initBuilder(tx2), builder); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts index 7d958a8b07..fd4d3950e4 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts @@ -1,658 +1,153 @@ 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 ExportInCTxBuilder', 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 ExportInCTxBuilder', function () { + it('should accept valid amounts in different formats', function () { + const validAmounts = [BigInt(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}`); - }); - }); - - 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']; - - 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}`); + txBuilder.amount(amount); + }); }); }); - it('should chain amount setting with other methods', function () { - const amount = '1000000000000000000'; - const nonce = 1n; + it('should throw error for invalid amounts', function () { + const invalidAmounts = ['0', '-1']; - 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 ExportInCTxBuilder', function () { + it('should accept valid nonces in different formats', function () { + const validNonces = ['1', 1, 0]; validNonces.forEach((nonce) => { assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept 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}`); + txBuilder.nonce(nonce); + }); }); }); - it('should chain nonce setting with other methods', function () { - const nonce = 123n; - const amount = '1000000000000000000'; - - assert.doesNotThrow(() => { - builder.nonce(nonce).amount(amount); - }); + it('should throw error for negative nonce', function () { + assert.throws(() => { + txBuilder.nonce('-1'); + }, new BuildTransactionError('Nonce must be greater or equal than 0')); }); }); - describe('Destination Address Management', function () { - it('should set single destination address', function () { - const singleAddress = 'P-flare1destination'; + describe('to ExportInCTxBuilder', function () { + const txBuilder = factory.getExportInCBuilder(); - assert.doesNotThrow(() => { - builder.to(singleAddress); - }); - }); - - it('should set multiple destination addresses', function () { - const multipleAddresses = ['P-flare1dest1', 'P-flare1dest2', 'P-flare1dest3']; + it('should accept multiple P-addresses', function () { + const pAddresses = testData.pAddresses; assert.doesNotThrow(() => { - builder.to(multipleAddresses); + txBuilder.to(pAddresses); }); }); - it('should handle comma-separated addresses', function () { - const commaSeparated = 'P-flare1dest1~P-flare1dest2~P-flare1dest3'; - + it('should accept single P-address', function () { assert.doesNotThrow(() => { - builder.to(commaSeparated); + txBuilder.to(testData.pAddresses[0]); }); }); - 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'; + it('should accept tilde-separated P-addresses string', function () { + const pAddresses = testData.pAddresses.join('~'); 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)`); + txBuilder.to(pAddresses); }); }); - - 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; - - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }, Error); - }); }); - 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: [], - }; - - assert.doesNotThrow(() => { - builder.initBuilder(mockRawTx); - }); - }); + 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 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: [], - }; + it('Should create export tx for same values', async () => { + const txBuilder = newTxBuilder(); - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, Error); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.unsignedHex); }); - 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: [], - }; + it('Should recover export tx from raw tx', async () => { + const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.unsignedHex); }); - 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); + xit('Should recover signed export from signed raw tx', async () => { + const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.signedHex); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.signedHex); }); - 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: [], - }; + it('Should full sign a export tx for same values', async () => { + const txBuilder = newTxBuilder(); - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); + 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); }); - }); - - describe('From Implementation', function () { - it('should handle string raw transaction', function () { - const rawString = 'hex-encoded-transaction'; - assert.doesNotThrow(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).fromImplementation(rawString); - }); + it('Should full sign a export tx from unsigned raw tx', async () => { + const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); + txBuilder.sign({ key: testData.privateKey }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.signedHex); }); - 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: [], - }; - - assert.doesNotThrow(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).fromImplementation(mockRawTx); + it('Key cannot sign the transaction', () => { + it('Should full sign a export tx from unsigned raw tx', () => { + const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')) + .from(testData.unsignedHex) + .fromPubKey(testData.pAddresses); + txBuilder.sign({ key: testData.privateKey }); + txBuilder + .build() + .then(() => assert.fail('it can sign')) + .catch((err) => { + err.message.should.be.equal('Private key cannot sign the transaction'); + }); }); }); }); - - 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); - }); - }); }); diff --git a/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts deleted file mode 100644 index 95d4dd30dc..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { coins } from '@bitgo/statics'; -import * as assert from 'assert'; -import { ExportInPTxBuilder } from '../../../src/lib/exportInPTxBuilder'; - -describe('ExportInPTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ExportInPTxBuilder; - - beforeEach(function () { - builder = new ExportInPTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ExportInPTxBuilder); - }); - - it('should extend AtomicTransactionBuilder', function () { - // Test inheritance - assert.ok(builder); - }); - - it('should initialize with default amount', function () { - // Default amount should be 0n - assert.ok(builder); - }); - }); - - describe('Amount Management', function () { - it('should set valid bigint amounts', function () { - const validAmounts = [1000n, 1000000000000000000n, 999999999999999999n]; // Removed 0n - - validAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept bigint amount: ${amount}`); - }); - }); - - it('should set string amounts', function () { - const stringAmounts = ['1000', '1000000000000000000', '999999999999999999']; // Removed '0' - - stringAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept string amount: ${amount}`); - }); - }); - - it('should set numeric amounts', function () { - const numericAmounts = [1000, 2000000, 999999999]; // Removed 0 - - numericAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept numeric amount: ${amount}`); - }); - }); - - it('should reject zero amount', function () { - assert.throws(() => { - builder.amount(0n); - }, /Amount must be positive/); - - assert.throws(() => { - builder.amount('0'); - }, /Amount must be positive/); - - assert.throws(() => { - builder.amount(0); - }, /Amount must be positive/); - }); - - it('should handle large amounts', function () { - const largeAmounts = [ - '100000000000000000000000', // Very large amount - '18446744073709551615', // Near uint64 max - BigInt('999999999999999999999999999999'), - 999999999999999999n, - ]; - - largeAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should handle large amount: ${amount}`); - }); - }); - - it('should reject negative amounts', function () { - const negativeAmounts = ['-1000', '-1']; - - negativeAmounts.forEach((amount) => { - assert.throws( - () => { - builder.amount(amount); - }, - Error, // validateAmount should throw error for negative amounts - `Should reject negative amount: ${amount}` - ); - }); - }); - - it('should reject negative bigint amounts', function () { - const negativeBigints = [-1n, -1000n, -999999999999n]; - - negativeBigints.forEach((amount) => { - assert.throws( - () => { - builder.amount(amount); - }, - Error, - `Should reject negative bigint amount: ${amount}` - ); - }); - }); - - it('should chain amount setting with other methods', function () { - const amount = 1000000000000000000n; // 1 FLR - - assert.doesNotThrow(() => { - builder.amount(amount); - }); - }); - }); - - describe('Transaction Type', function () { - it('should return Export transaction type', function () { - // Can't access protected method directly, but test doesn't throw - assert.ok(builder); - }); - }); - - describe('Transaction Initialization', function () { - it('should initialize builder from transaction', function () { - const mockTx = { type: 'export', data: 'placeholder' }; - - assert.doesNotThrow(() => { - builder.initBuilder(mockTx); - }); - }); - - it('should handle null transaction initialization', function () { - assert.doesNotThrow(() => { - builder.initBuilder(null); - }); - }); - - it('should handle undefined transaction initialization', function () { - assert.doesNotThrow(() => { - builder.initBuilder(undefined); - }); - }); - - it('should handle object transaction initialization', function () { - const mockTx = { - id: 'test-tx', - type: 'export', - amount: '1000000000000000000', - from: 'P-flare1source', - to: 'C-flare1dest', - }; - - assert.doesNotThrow(() => { - builder.initBuilder(mockTx); - }); - }); - }); - - describe('Transaction Type Verification', function () { - it('should verify transaction type (returns false - not implemented)', function () { - const mockTx = { type: 'export' }; - const result = ExportInPTxBuilder.verifyTxType(mockTx); - assert.strictEqual(result, false); // Not implemented yet - }); - - it('should handle different transaction objects', function () { - const testCases = [{}, null, undefined, { type: 'import' }, { data: 'test' }]; - - testCases.forEach((testCase, index) => { - const result = ExportInPTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, false, `Test case ${index} should return false (not implemented)`); - }); - }); - - it('should verify via instance method', function () { - const mockTx = { type: 'export' }; - const result = builder.verifyTxType(mockTx); - assert.strictEqual(result, false); // Not implemented - }); - - it('should verify static and instance methods return same result', function () { - const mockTx = { type: 'export' }; - const staticResult = ExportInPTxBuilder.verifyTxType(mockTx); - const instanceResult = builder.verifyTxType(mockTx); - assert.strictEqual(staticResult, instanceResult); - }); - }); - - describe('Transaction Building', function () { - it('should throw error when building (not implemented)', function () { - builder.amount(1000000000000000000n); - - assert.throws(() => { - // Access protected method for testing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }, Error); - }); - - it('should throw specific error message', function () { - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }, /Flare P-chain export transaction build not implemented/); - }); - }); - - describe('Exported Outputs', function () { - it('should return empty array for exported outputs', function () { - // Access protected method for testing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (builder as any).exportedOutputs(); - - assert.ok(Array.isArray(result)); - assert.strictEqual(result.length, 0); - }); - - it('should consistently return empty array', function () { - // Call multiple times to ensure consistency - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result1 = (builder as any).exportedOutputs(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result2 = (builder as any).exportedOutputs(); - - assert.deepStrictEqual(result1, result2); - }); - }); - - describe('Amount Validation', function () { - it('should validate positive amounts', function () { - const positiveAmounts = [1n, 1000n, 1000000000000000000n]; - - positiveAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should validate positive amount: ${amount}`); - }); - }); - - it('should reject zero amount', function () { - assert.throws(() => { - builder.amount(0n); - }, /Amount must be positive/); - }); - - it('should validate large amounts', function () { - const largeAmounts = [ - BigInt('18446744073709551615'), // Max uint64 - BigInt('999999999999999999999999999999'), // Very large - ]; - - largeAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should validate large amount: ${amount}`); - }); - }); - }); - - describe('Integration Tests', function () { - it('should handle basic P-chain export setup', function () { - const amount = 5000000000000000000n; // 5 FLR - - // Basic setup - builder.amount(amount); - - // Should not throw during setup - assert.ok(true); - }); - - it('should handle amount conversion from different types', function () { - // Test conversion from string - builder.amount('1000000000000000000'); - - // Test conversion from number - builder.amount(1000000); - - // Test direct bigint - builder.amount(2000000000000000000n); - - // All conversions should work - assert.ok(true); - }); - - it('should maintain state across operations', function () { - // Set amount - builder.amount(1000000000000000000n); - - // Initialize with transaction - const mockTx = { type: 'export' }; - builder.initBuilder(mockTx); - - // State should be maintained - assert.ok(true); - }); - - it('should handle sequential amount updates', function () { - // Update amount multiple times - builder.amount(1000n); - builder.amount(2000n); - builder.amount(3000n); - - // Should handle updates without issues - assert.ok(true); - }); - }); - - describe('Error Handling', function () { - it('should handle invalid amount types gracefully', function () { - const invalidAmounts = [null, undefined, {}, [], 'invalid']; - - invalidAmounts.forEach((amount) => { - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - builder.amount(amount as any); - }, `Should throw for invalid amount: ${amount}`); - }); - }); - - it('should handle edge case string amounts', function () { - const edgeCaseAmounts = ['', ' ', 'abc', '1.5', 'infinity', 'NaN']; - - edgeCaseAmounts.forEach((amount) => { - assert.throws(() => { - builder.amount(amount); - }, `Should throw for edge case amount: ${amount}`); - }); - }); - }); - - describe('Inheritance Tests', function () { - it('should inherit from AtomicTransactionBuilder', function () { - // Test that builder has expected inherited functionality - assert.ok(builder); - - // Should have initBuilder method - assert.ok(typeof builder.initBuilder === 'function'); - - // Should have verifyTxType method - assert.ok(typeof builder.verifyTxType === 'function'); - }); - - it('should have access to inherited UTXO methods', function () { - // Should inherit utxos method from parent - assert.ok(typeof builder.utxos === 'function'); - }); - }); - - describe('Edge Cases', function () { - it('should handle rapid consecutive operations', function () { - // Rapid amount changes (start from 1 since 0 is not valid) - for (let i = 1; i <= 100; i++) { - builder.amount(BigInt(i * 1000)); - } - - // Should handle rapid operations - assert.ok(true); - }); - - it('should handle maximum bigint values', function () { - const maxValues = [ - BigInt(Number.MAX_SAFE_INTEGER), - BigInt('18446744073709551615'), // Max uint64 - ]; - - maxValues.forEach((value) => { - assert.doesNotThrow(() => { - builder.amount(value); - }, `Should handle max value: ${value}`); - }); - }); - - it('should handle minimum valid values', function () { - const minValues = [1n]; // Removed 0n since it's not valid - - minValues.forEach((value) => { - assert.doesNotThrow(() => { - builder.amount(value); - }, `Should handle min value: ${value}`); - }); - }); - }); - - describe('Future Implementation Readiness', function () { - it('should be ready for future buildFlareTransaction implementation', function () { - // Setup valid state - builder.amount(1000000000000000000n); - - // Current implementation throws, but structure is in place - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }, /not implemented/); - }); - - it('should be ready for future verifyTxType implementation', function () { - // Current implementation returns false - const result = builder.verifyTxType({ type: 'export' }); - assert.strictEqual(result, false); - - // But method signature is ready for implementation - assert.ok(typeof builder.verifyTxType === 'function'); - }); - - it('should be ready for future exportedOutputs implementation', function () { - // Current implementation returns empty array - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (builder as any).exportedOutputs(); - assert.ok(Array.isArray(result)); - - // But method exists and is ready for implementation - assert.strictEqual(result.length, 0); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts deleted file mode 100644 index 9f1335fd4a..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { ImportInCTxBuilder } from '../../../src/lib/importInCTxBuilder'; -import { DecodedUtxoObj } from '../../../src/lib/iface'; - -describe('ImportInCTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ImportInCTxBuilder; - - beforeEach(function () { - builder = new ImportInCTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ImportInCTxBuilder); - }); - }); - - describe('Address Management', function () { - it('should set valid C-chain destination address', function () { - const validAddresses = [ - 'P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh', - 'P-flare1abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef123456', - 'NodeID-flare1test123456789abcdef', - ]; - - validAddresses.forEach((address) => { - assert.doesNotThrow(() => { - builder.to(address); - }, `Should accept valid address: ${address}`); - }); - }); - - it('should reject invalid C-chain addresses', function () { - const invalidAddresses = [ - '', - 'invalid-address', - '0x742C4B18dd62F23BF0bf8c183f4D5E5F6c6c46f8', // Ethereum format - 'C-invalid-format', - 'flare1test', // Missing prefix - 'random-string-123', - ]; - - invalidAddresses.forEach((address) => { - assert.throws( - () => { - builder.to(address); - }, - BuildTransactionError, - `Should reject invalid address: ${address}` - ); - }); - }); - - it('should handle multiple address settings', function () { - const address1 = 'P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh'; - const address2 = 'P-flare1abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef123456'; - - builder.to(address1); - builder.to(address2); // Should overwrite the first address - - // Test doesn't throw - assert.ok(true); - }); - }); - - describe('UTXO Management', function () { - it('should add single UTXO', function () { - const utxo: DecodedUtxoObj = { - outputID: 1, - amount: '1000000000000000000', // 1 FLR - txid: 'test-txid-single', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test'], - }; - - assert.doesNotThrow(() => { - builder.addUtxos([utxo]); - }); - }); - - it('should add multiple UTXOs', function () { - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', // 1 FLR - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test1'], - }, - { - outputID: 2, - amount: '2000000000000000000', // 2 FLR - txid: 'test-txid-2', - outputidx: '1', - threshold: 2, - addresses: ['P-flare1test2', 'P-flare1test3'], - }, - { - outputID: 3, - amount: '500000000000000000', // 0.5 FLR - txid: 'test-txid-3', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test4'], - }, - ]; - - assert.doesNotThrow(() => { - builder.addUtxos(utxos); - }); - }); - - it('should handle empty UTXO array', function () { - assert.doesNotThrow(() => { - builder.addUtxos([]); - }); - }); - - it('should accumulate UTXOs from multiple calls', function () { - const utxos1: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test1'], - }, - ]; - - const utxos2: DecodedUtxoObj[] = [ - { - outputID: 2, - amount: '2000000000000000000', - txid: 'test-txid-2', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test2'], - }, - ]; - - builder.addUtxos(utxos1); - builder.addUtxos(utxos2); - - // Test doesn't throw - assert.ok(true); - }); - }); - - describe('Source Chain Management', function () { - // TODO : Enable these tests after fixing sourceChain method to accept P-chain IDs - it.skip('should set valid source chain IDs', function () { - const validChainIds = ['P-flare12345', 'NodeID-flare67890', '0x123456789abcdef', 'abc123def456']; - - validChainIds.forEach((chainId) => { - assert.doesNotThrow(() => { - builder.sourceChain(chainId); - }, `Should accept chain ID: ${chainId}`); - }); - }); - - it('should handle different chain ID formats', function () { - // Test various formats that might be encountered - const chainIds = ['P-chain-id-123', 'C-chain-id-456', 'hex-formatted-id', '1234567890abcdef']; - - chainIds.forEach((chainId) => { - assert.doesNotThrow(() => { - builder.sourceChain(chainId); - }); - }); - }); - }); - - describe('Transaction Type Verification', function () { - it('should verify transaction type (placeholder)', function () { - const mockTx = { type: 'import' }; - const result = ImportInCTxBuilder.verifyTxType(mockTx); - assert.strictEqual(result, true); // Placeholder returns true - }); - - it('should handle different transaction objects', function () { - const validCases = [ - { type: 'import' }, // Placeholder import transaction - { - importIns: [{ input: 'test' }], - sourceChain: 'P-chain', - to: '0x1234567890123456789012345678901234567890', - }, // Realistic import transaction - ]; - - const invalidCases = [{}, null, undefined, { type: 'export' }, { data: 'test' }]; - - validCases.forEach((testCase, index) => { - const result = ImportInCTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, true, `Valid test case ${index} should return true`); - }); - - invalidCases.forEach((testCase, index) => { - const result = ImportInCTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, false, `Invalid test case ${index} should return false`); - }); - }); - - it('should verify via instance method', function () { - const mockTx = { type: 'import' }; - const result = builder.verifyTxType(mockTx); - assert.strictEqual(result, true); - }); - }); - - describe('Integration Tests', function () { - it('should handle complete import flow preparation', function () { - const address = 'P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh'; - const chainId = 'P-source-chain-123'; - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '5000000000000000000', // 5 FLR - txid: 'integration-test-txid', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1source'], - }, - ]; - - // Complete setup - builder.to(address); - builder.sourceChain(chainId); - builder.addUtxos(utxos); - - // All operations should complete without throwing - assert.ok(true); - }); - - it('should handle method chaining', function () { - const address = 'P-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh'; - const chainId = 'P-chain-123'; - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'chain-test-txid', - outputidx: '0', - threshold: 1, - addresses: ['P-flare1test'], - }, - ]; - - // Test method chaining - assert.doesNotThrow(() => { - builder.to(address).sourceChain(chainId).addUtxos(utxos); - }); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts deleted file mode 100644 index 62a0dd23c7..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import BigNumber from 'bignumber.js'; -import { ImportInPTxBuilder } from '../../../src/lib/importInPTxBuilder'; -import { DecodedUtxoObj } from '../../../src/lib/iface'; - -describe('ImportInPTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ImportInPTxBuilder; - - beforeEach(function () { - builder = new ImportInPTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ImportInPTxBuilder); - }); - - it('should initialize with default values', function () { - // Builder should be initialized without throwing errors - assert.ok(builder); - }); - }); - - describe('UTXO Management', function () { - it('should add single UTXO', function () { - const utxo: DecodedUtxoObj = { - outputID: 1, - amount: '1000000000000000000', // 1 FLR - txid: 'test-txid-single', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test'], - }; - - assert.doesNotThrow(() => { - builder.addUtxos([utxo]); - }); - }); - - it('should add multiple UTXOs', function () { - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', // 1 FLR - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test1'], - }, - { - outputID: 2, - amount: '2000000000000000000', // 2 FLR - txid: 'test-txid-2', - outputidx: '1', - threshold: 2, - addresses: ['C-flare1test2', 'C-flare1test3'], - }, - { - outputID: 3, - amount: '500000000000000000', // 0.5 FLR - txid: 'test-txid-3', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test4'], - }, - ]; - - assert.doesNotThrow(() => { - builder.addUtxos(utxos); - }); - }); - - it('should handle empty UTXO array', function () { - assert.doesNotThrow(() => { - builder.addUtxos([]); - }); - }); - - it('should accumulate UTXOs from multiple calls', function () { - const utxos1: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test1'], - }, - ]; - - const utxos2: DecodedUtxoObj[] = [ - { - outputID: 2, - amount: '2000000000000000000', - txid: 'test-txid-2', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test2'], - }, - ]; - - builder.addUtxos(utxos1); - builder.addUtxos(utxos2); - - // Should not throw - assert.ok(true); - }); - }); - - describe('Fee Management', function () { - it('should set valid positive fee', function () { - const validFees = ['1000', '0', '1000000000000000000', '999999999999999999']; - - validFees.forEach((fee) => { - assert.doesNotThrow(() => { - builder.fee(fee); - }, `Should accept fee: ${fee}`); - }); - }); - - it('should reject negative fees', function () { - const negativeFees = ['-1', '-1000', '-1000000000000000000']; - - negativeFees.forEach((fee) => { - assert.throws( - () => { - builder.fee(fee); - }, - BuildTransactionError, - `Should reject negative fee: ${fee}` - ); - }); - }); - - it('should handle invalid fee formats (BigNumber behavior)', function () { - const invalidFees = ['abc', 'not-a-number', '1.5.5', 'infinity']; - - invalidFees.forEach((fee) => { - // BigNumber doesn't throw for invalid strings, it creates NaN values - // But our implementation should still accept them (no additional validation) - assert.doesNotThrow(() => { - builder.fee(fee); - }, `BigNumber accepts invalid strings: ${fee}`); - }); - }); - - it('should handle BigNumber fee inputs', function () { - const bigNumberFee = new BigNumber('1000000000000000000'); - assert.doesNotThrow(() => { - builder.fee(bigNumberFee.toString()); - }); - }); - - it('should chain fee setting with other methods', function () { - const fee = '1000'; - const utxo: DecodedUtxoObj = { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test'], - }; - - assert.doesNotThrow(() => { - builder.fee(fee).addUtxos([utxo]); - }); - }); - }); - - describe('Locktime Management', function () { - it('should set valid locktime values', function () { - const validLocktimes = [0, 1, 1000, 4294967295]; // Max uint32 - - validLocktimes.forEach((locktime) => { - assert.doesNotThrow(() => { - builder.locktime(locktime); - }, `Should accept locktime: ${locktime}`); - }); - }); - - it('should handle negative locktime (no validation)', function () { - const negativeLocktimes = [-1, -1000]; - - negativeLocktimes.forEach((locktime) => { - assert.doesNotThrow(() => { - builder.locktime(locktime); - }, `Should accept negative locktime: ${locktime} (no validation in implementation)`); - }); - }); - - it('should handle boundary values', function () { - const boundaryValues = [0, 4294967295]; // Min and max uint32 - - boundaryValues.forEach((locktime) => { - assert.doesNotThrow(() => { - builder.locktime(locktime); - }); - }); - }); - - it('should chain locktime setting with other methods', function () { - const locktime = 123456; - const fee = '1000'; - - assert.doesNotThrow(() => { - builder.locktime(locktime).fee(fee); - }); - }); - }); - - describe('Threshold Management', function () { - it('should set valid threshold values', function () { - const validThresholds = [1, 2, 5, 10]; - - validThresholds.forEach((threshold) => { - assert.doesNotThrow(() => { - builder.threshold(threshold); - }, `Should accept threshold: ${threshold}`); - }); - }); - - it('should reject invalid threshold values', function () { - const invalidThresholds = [0, -1, -10]; - - invalidThresholds.forEach((threshold) => { - assert.throws( - () => { - builder.threshold(threshold); - }, - BuildTransactionError, - `Should reject threshold: ${threshold}` - ); - }); - }); - - it('should handle typical threshold scenarios', function () { - // Single signature - assert.doesNotThrow(() => { - builder.threshold(1); - }); - - // Multi-signature - assert.doesNotThrow(() => { - builder.threshold(3); - }); - }); - - it('should chain threshold setting with other methods', function () { - const threshold = 2; - const locktime = 123456; - - assert.doesNotThrow(() => { - builder.threshold(threshold).locktime(locktime); - }); - }); - }); - - describe('Source Chain Management', function () { - // TODO : Enable these tests after fixing sourceChain method to accept P-chain IDs - it.skip('should set valid source chain IDs', function () { - const validChainIds = ['C-flare12345', 'NodeID-flare67890', '0x123456789abcdef', 'abc123def456']; - - validChainIds.forEach((chainId) => { - assert.doesNotThrow(() => { - builder.sourceChain(chainId); - }, `Should accept chain ID: ${chainId}`); - }); - }); - - it('should handle different chain ID formats', function () { - const chainIds = ['C-chain-id-123', 'P-chain-id-456', 'hex-formatted-id', '1234567890abcdef']; - - chainIds.forEach((chainId) => { - assert.doesNotThrow(() => { - builder.sourceChain(chainId); - }); - }); - }); - }); - - describe('Input/Output Creation', function () { - it('should test public methods only due to protected access', function () { - // Note: createInputOutput is protected, so we test through public interface - // Test that we can set parameters that would be used by createInputOutput - assert.doesNotThrow(() => { - builder.fee('1000'); - builder.threshold(1); - // Protected method cannot be tested directly - }); - }); - - it('should handle configuration for output creation', function () { - const fee = '1000'; - const locktime = 0; - const threshold = 1; - - assert.doesNotThrow(() => { - builder.fee(fee).locktime(locktime).threshold(threshold); - }); - }); - - it('should allow setting parameters for large amounts', function () { - const locktime = 0; - const threshold = 1; - - assert.doesNotThrow(() => { - builder.fee('1000').locktime(locktime).threshold(threshold); - }); - }); - - it('should configure for different threshold scenarios', function () { - const locktime = 0; - - // Test different thresholds - [1, 2, 3].forEach((threshold) => { - assert.doesNotThrow(() => { - builder.threshold(threshold).locktime(locktime); - }); - }); - }); - }); - - describe('Transaction Type Verification', function () { - it('should verify transaction type (placeholder)', function () { - const mockTx = { type: 'import' }; - const result = ImportInPTxBuilder.verifyTxType(mockTx); - assert.strictEqual(result, true); // Placeholder returns true - }); - - it('should handle different transaction objects', function () { - const testCases = [{}, null, undefined, { type: 'export' }, { data: 'test' }]; - - testCases.forEach((testCase, index) => { - const result = ImportInPTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, true, `Test case ${index} should return true (placeholder)`); - }); - }); - - it('should verify via instance method', function () { - const mockTx = { type: 'import' }; - const result = builder.verifyTxType(mockTx); - assert.strictEqual(result, true); - }); - }); - - describe('Transaction Building Preparation', function () { - it('should prepare basic import transaction parameters', function () { - const fee = '1000'; - const locktime = 123456; - const threshold = 2; - const chainId = 'C-source-chain-123'; - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test'], - }, - ]; - - // Set all parameters - builder.fee(fee).locktime(locktime).threshold(threshold).sourceChain(chainId).addUtxos(utxos); - - // Should not throw - assert.ok(true); - }); - - it('should handle minimal configuration', function () { - const fee = '0'; - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test'], - }, - ]; - - builder.fee(fee).addUtxos(utxos); - - // Should not throw - assert.ok(true); - }); - }); - - describe('Complex Scenarios', function () { - it('should handle multiple UTXOs with different properties', function () { - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1single'], - }, - { - outputID: 2, - amount: '2000000000000000000', - txid: 'test-txid-2', - outputidx: '1', - threshold: 2, - addresses: ['C-flare1multi1', 'C-flare1multi2'], - }, - { - outputID: 3, - amount: '3000000000000000000', - txid: 'test-txid-3', - outputidx: '0', - threshold: 3, - addresses: ['C-flare1multi1', 'C-flare1multi2', 'C-flare1multi3'], - }, - ]; - - builder.addUtxos(utxos); - builder.fee('5000'); // Higher fee for complex transaction - builder.threshold(2); - - // Should handle complex UTXO set - assert.ok(true); - }); - - it('should handle large transaction parameters', function () { - const fee = '1000000000000000000'; // 1 FLR fee - const locktime = 4294967295; // Max uint32 - const threshold = 10; // High threshold - const chainId = 'very-long-chain-id-with-lots-of-characters-0x123456789abcdef0123456789abcdef'; - - builder.fee(fee).locktime(locktime).threshold(threshold).sourceChain(chainId); - - // Should handle large values - assert.ok(true); - }); - - it('should handle rapid parameter changes', function () { - // Simulate rapid parameter updates - builder.fee('1000').fee('2000').fee('3000'); - builder.locktime(100).locktime(200).locktime(300); - builder.threshold(1).threshold(2).threshold(3); - - // Should handle rapid changes without issues - assert.ok(true); - }); - }); - - describe('Edge Cases', function () { - it('should handle zero values where appropriate', function () { - builder.fee('0'); - builder.locktime(0); - // threshold of 0 should be invalid - assert.throws(() => { - builder.threshold(0); - }, BuildTransactionError); - }); - - it('should handle maximum values', function () { - const maxFee = '115792089237316195423570985008687907853269984665640564039457584007913129639935'; // Max uint256 - const maxLocktime = 4294967295; // Max uint32 - - assert.doesNotThrow(() => { - builder.fee(maxFee); - }); - - assert.doesNotThrow(() => { - builder.locktime(maxLocktime); - }); - }); - - it('should maintain state across multiple operations', function () { - const utxo1: DecodedUtxoObj = { - outputID: 1, - amount: '1000000000000000000', - txid: 'test-txid-1', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test1'], - }; - - const utxo2: DecodedUtxoObj = { - outputID: 2, - amount: '2000000000000000000', - txid: 'test-txid-2', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test2'], - }; - - // Build state incrementally - builder.fee('1000'); - builder.addUtxos([utxo1]); - builder.locktime(123456); - builder.addUtxos([utxo2]); - builder.threshold(2); - - // State should be maintained across operations - assert.ok(true); - }); - }); - - describe('Integration Tests', function () { - it('should handle complete P-chain import flow preparation', function () { - const fee = '2000'; - const locktime = 654321; - const threshold = 1; - const chainId = 'C-source-chain-456'; - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '6000000000000000000', // 6 FLR (more than output for fees) - txid: 'integration-test-txid', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1source'], - }, - ]; - - // Complete setup - builder.fee(fee).locktime(locktime).threshold(threshold).sourceChain(chainId).addUtxos(utxos); - - // All operations should complete without throwing - assert.ok(true); - }); - - it('should handle method chaining extensively', function () { - const utxos: DecodedUtxoObj[] = [ - { - outputID: 1, - amount: '10000000000000000000', // 10 FLR - txid: 'chain-test-txid', - outputidx: '0', - threshold: 1, - addresses: ['C-flare1test'], - }, - ]; - - // Test extensive method chaining - assert.doesNotThrow(() => { - builder.fee('1000').locktime(100000).threshold(1).sourceChain('C-chain-123').addUtxos(utxos); - }); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/transaction.ts b/modules/sdk-coin-flrp/test/unit/lib/transaction.ts deleted file mode 100644 index 76422eb16c..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/transaction.ts +++ /dev/null @@ -1,509 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { Transaction } from '../../../src/lib/transaction'; -import { KeyPair } from '../../../src/lib/keyPair'; - -// Mock transaction for testing -interface MockTx { - test: string; -} - -describe('FLRP Transaction', function () { - let transaction: Transaction; - const coinConfig = coins.get('tflrp'); - - // Helper to create a mock transaction for testing - const createMockTx = (): MockTx => ({ test: 'transaction' }); - - beforeEach(function () { - transaction = new Transaction(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with correct network configuration', function () { - assert.strictEqual(transaction._assetId, 'FLR'); - assert.strictEqual(transaction._blockchainID, '11111111111111111111111111111111LpoYY'); - assert.strictEqual(transaction._networkID, 114); - assert.strictEqual(transaction._threshold, 2); - assert.strictEqual(transaction._locktime, BigInt(0)); - }); - - it('should initialize empty arrays and default values', function () { - assert.deepStrictEqual(transaction._fromAddresses, []); - assert.deepStrictEqual(transaction._rewardAddresses, []); - assert.deepStrictEqual(transaction._utxos, []); - assert.deepStrictEqual(transaction._fee, {}); - }); - }); - - describe('Transaction Type Management', function () { - it('should set supported transaction types', function () { - const supportedTypes = [ - TransactionType.Export, - TransactionType.Import, - TransactionType.AddValidator, - TransactionType.AddDelegator, - TransactionType.AddPermissionlessValidator, - TransactionType.AddPermissionlessDelegator, - ]; - - supportedTypes.forEach((type) => { - assert.doesNotThrow(() => { - transaction.setTransactionType(type); - assert.strictEqual(transaction._type, type); - }); - }); - }); - - it('should throw error for unsupported transaction types', function () { - assert.throws(() => { - transaction.setTransactionType(TransactionType.Send); - }, /Transaction type .* is not supported/); - }); - }); - - describe('Getters', function () { - it('should return correct flareTransaction', function () { - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - assert.strictEqual(transaction.flareTransaction, mockTx); - }); - - it('should return empty signature array when no credentials', function () { - assert.deepStrictEqual(transaction.signature, []); - }); - - it('should return empty credentials array', function () { - assert.deepStrictEqual(transaction.credentials, []); - }); - - it('should return false for hasCredentials when no credentials', function () { - assert.strictEqual(transaction.hasCredentials, false); - }); - - it('should return placeholder ID when no transaction set', function () { - // This should throw error when no transaction is set - assert.throws(() => { - transaction.id; - }, InvalidTransactionError); - }); - - it('should return placeholder signablePayload when no transaction set', function () { - // This should throw error when no transaction is set - assert.throws(() => { - void transaction.signablePayload; - }, InvalidTransactionError); - }); - - it('should return formatted fromAddresses', function () { - transaction._fromAddresses = ['address1', 'address2']; - const addresses = transaction.fromAddresses; - assert.deepStrictEqual(addresses, ['address1', 'address2']); - }); - - it('should return formatted rewardAddresses', function () { - transaction._rewardAddresses = ['reward1', 'reward2']; - const addresses = transaction.rewardAddresses; - assert.deepStrictEqual(addresses, ['reward1', 'reward2']); - }); - }); - - describe('Transaction Outputs', function () { - beforeEach(function () { - transaction._nodeID = 'test-node-id'; - transaction._stakeAmount = BigInt('1000000000000000'); // 1M FLR - }); - - it('should return empty outputs for Export type', function () { - transaction.setTransactionType(TransactionType.Export); - const outputs = transaction.outputs; - assert.deepStrictEqual(outputs, []); - }); - - it('should return empty outputs for Import type', function () { - transaction.setTransactionType(TransactionType.Import); - const outputs = transaction.outputs; - assert.deepStrictEqual(outputs, []); - }); - - it('should return staking outputs for AddValidator type', function () { - transaction.setTransactionType(TransactionType.AddValidator); - const outputs = transaction.outputs; - assert.strictEqual(outputs.length, 1); - assert.strictEqual(outputs[0].address, 'test-node-id'); - assert.strictEqual(outputs[0].value, '1000000000000000'); - }); - - it('should return staking outputs for AddPermissionlessValidator type', function () { - transaction.setTransactionType(TransactionType.AddPermissionlessValidator); - const outputs = transaction.outputs; - assert.strictEqual(outputs.length, 1); - assert.strictEqual(outputs[0].address, 'test-node-id'); - assert.strictEqual(outputs[0].value, '1000000000000000'); - }); - - it('should return staking outputs for AddDelegator type', function () { - transaction.setTransactionType(TransactionType.AddDelegator); - const outputs = transaction.outputs; - assert.strictEqual(outputs.length, 1); - assert.strictEqual(outputs[0].address, 'test-node-id'); - assert.strictEqual(outputs[0].value, '1000000000000000'); - }); - - it('should return staking outputs for AddPermissionlessDelegator type', function () { - transaction.setTransactionType(TransactionType.AddPermissionlessDelegator); - const outputs = transaction.outputs; - assert.strictEqual(outputs.length, 1); - assert.strictEqual(outputs[0].address, 'test-node-id'); - assert.strictEqual(outputs[0].value, '1000000000000000'); - }); - - it('should return empty outputs for unknown type', function () { - // Don't set type, should default to empty - const outputs = transaction.outputs; - assert.deepStrictEqual(outputs, []); - }); - }); - - describe('Transaction Inputs', function () { - it('should return inputs from UTXOs', function () { - transaction._utxos = [ - { - outputID: 1, - amount: '1000000', - txid: 'test-txid-1', - outputidx: '0', - threshold: 2, - addresses: ['addr1', 'addr2'], - }, - { - outputID: 2, - amount: '2000000', - txid: 'test-txid-2', - outputidx: '1', - threshold: 2, - addresses: ['addr3', 'addr4'], - }, - ]; - transaction._fromAddresses = ['addr1', 'addr2', 'addr3', 'addr4']; - - const inputs = transaction.inputs; - assert.strictEqual(inputs.length, 2); - assert.strictEqual(inputs[0].id, 'test-txid-1:0'); - assert.strictEqual(inputs[0].value, '1000000'); - assert.strictEqual(inputs[0].address, 'addr1~addr2~addr3~addr4'); - assert.strictEqual(inputs[1].id, 'test-txid-2:1'); - assert.strictEqual(inputs[1].value, '2000000'); - }); - - it('should return empty inputs when no UTXOs', function () { - const inputs = transaction.inputs; - assert.deepStrictEqual(inputs, []); - }); - }); - - describe('Change Outputs', function () { - it('should return empty change outputs', function () { - const changeOutputs = transaction.changeOutputs; - assert.deepStrictEqual(changeOutputs, []); - }); - }); - - describe('Fee Handling', function () { - it('should return default fee when no fee set', function () { - const fee = transaction.fee; - assert.deepStrictEqual(fee, { fee: '0' }); - }); - - it('should return custom fee when set', function () { - transaction._fee = { fee: '1000000' }; - const fee = transaction.fee; - assert.deepStrictEqual(fee, { fee: '1000000' }); - }); - }); - - describe('Signing', function () { - let keyPair: KeyPair; - - beforeEach(function () { - // Create a test key pair with a private key - keyPair = new KeyPair({ prv: '01'.repeat(32) }); - }); - - it('should throw error when no private key', async function () { - const emptyKeyPair = new KeyPair(); - - await assert.rejects(async () => { - await transaction.sign(emptyKeyPair); - }, InvalidTransactionError); // Will throw InvalidTransactionError for empty transaction first - }); - - it('should throw error when no transaction to sign', async function () { - await assert.rejects(async () => { - await transaction.sign(keyPair); - }, InvalidTransactionError); - }); - - it('should throw error for FlareJS signing not implemented', async function () { - // Set a mock transaction and mock credentials to pass validation - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - - // Mock hasCredentials to return true - Object.defineProperty(transaction, 'hasCredentials', { - get: () => true, - }); - - await assert.rejects(async () => { - await transaction.sign(keyPair); - }, /FlareJS signing not yet implemented/); - }); - }); - - describe('Serialization', function () { - it('should throw error for toBroadcastFormat when no transaction', function () { - assert.throws(() => { - transaction.toBroadcastFormat(); - }, InvalidTransactionError); - }); - - it('should return placeholder for toBroadcastFormat when transaction set', function () { - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - const broadcastFormat = transaction.toBroadcastFormat(); - assert.strictEqual(broadcastFormat, 'flare-tx-hex-placeholder'); - }); - - it('should throw error for toJson when no transaction', function () { - assert.throws(() => { - transaction.toJson(); - }, InvalidTransactionError); - }); - - it('should return transaction data for toJson when transaction set', function () { - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - transaction._fromAddresses = ['addr1']; - transaction._threshold = 2; - transaction._locktime = BigInt(100); - - const jsonData = transaction.toJson(); - assert.strictEqual(jsonData.id, 'flare-transaction-id-placeholder'); - assert.deepStrictEqual(jsonData.fromAddresses, ['addr1']); - assert.strictEqual(jsonData.threshold, 2); - assert.strictEqual(jsonData.locktime, '100'); - }); - }); - - describe('Cross-chain Properties', function () { - it('should identify Export as cross-chain transaction', function () { - transaction.setTransactionType(TransactionType.Export); - assert.strictEqual(transaction.isTransactionForCChain, true); - }); - - it('should identify Import as cross-chain transaction', function () { - transaction.setTransactionType(TransactionType.Import); - assert.strictEqual(transaction.isTransactionForCChain, true); - }); - - it('should identify AddValidator as non-cross-chain transaction', function () { - transaction.setTransactionType(TransactionType.AddValidator); - assert.strictEqual(transaction.isTransactionForCChain, false); - }); - }); - - describe('Transaction Explanation', function () { - beforeEach(function () { - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - transaction._fromAddresses = ['test-address']; - transaction._fee = { fee: '1000000' }; - }); - - it('should explain a basic transaction', function () { - transaction.setTransactionType(TransactionType.Export); - - const explanation = transaction.explainTransaction(); - assert.ok(explanation.displayOrder); - assert.ok(Array.isArray(explanation.displayOrder)); - assert.strictEqual(explanation.id, 'flare-transaction-id-placeholder'); - assert.deepStrictEqual(explanation.fee, { fee: '1000000' }); - assert.strictEqual(explanation.type, TransactionType.Export); - }); - - it('should include reward addresses for staking transactions', function () { - transaction.setTransactionType(TransactionType.AddValidator); - transaction._rewardAddresses = ['reward-addr-1']; - - const explanation = transaction.explainTransaction(); - assert.ok(explanation.displayOrder); - assert.ok(explanation.displayOrder.includes('rewardAddresses')); - assert.deepStrictEqual(explanation.rewardAddresses, ['reward-addr-1']); - }); - - it('should include cross-chain information for export/import', function () { - transaction.setTransactionType(TransactionType.Export); - - const explanation = transaction.explainTransaction(); - assert.ok(explanation.displayOrder); - assert.ok(explanation.displayOrder.includes('sourceChain')); - assert.ok(explanation.displayOrder.includes('destinationChain')); - }); - - it('should calculate output amounts correctly', function () { - transaction._nodeID = 'test-node'; - transaction._stakeAmount = BigInt('5000000000000000'); - transaction.setTransactionType(TransactionType.AddValidator); - - const explanation = transaction.explainTransaction(); - assert.strictEqual(explanation.outputAmount, '5000000000000000'); - assert.strictEqual(explanation.changeAmount, '0'); - }); - }); - - describe('Signature Creation', function () { - it('should create signature placeholder', function () { - const mockPrivateKey = Buffer.from('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', 'hex'); - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - - const signature = transaction.createSignature(mockPrivateKey); - assert.ok(typeof signature === 'string'); - assert.ok(signature.length > 0); - }); - }); - - describe('Validation', function () { - it('should allow signing with valid key', function () { - const keyPair = new KeyPair({ prv: '01'.repeat(32) }); - - const canSign = transaction.canSign({ key: keyPair }); - assert.strictEqual(canSign, true); - }); - }); - - describe('Transaction Setting', function () { - it('should set transaction correctly', function () { - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - // Can't directly access _flareTransaction (protected), but can test via getter - assert.strictEqual(transaction.flareTransaction, mockTx); - }); - }); - - describe('Hex Conversion', function () { - it('should convert byte array to hex string', function () { - const byteArray = new Uint8Array([0x01, 0x23, 0xab, 0xcd]); - const hexString = transaction.toHexString(byteArray); - assert.strictEqual(hexString, '0123abcd'); - }); - - it('should handle empty byte array', function () { - const byteArray = new Uint8Array([]); - const hexString = transaction.toHexString(byteArray); - assert.strictEqual(hexString, ''); - }); - }); - - describe('Memo Functionality', function () { - it('should initialize with empty memo', function () { - assert.strictEqual(transaction.hasMemo(), false); - assert.strictEqual(transaction.getMemoString(), ''); - assert.deepStrictEqual(transaction.getMemoBytes(), new Uint8Array()); - }); - - it('should set memo from string', function () { - const memoText = 'Test transaction memo'; - transaction.setMemo(memoText); - - assert.strictEqual(transaction.hasMemo(), true); - assert.strictEqual(transaction.getMemoString(), memoText); - assert.deepStrictEqual(transaction.getMemoBytes(), new TextEncoder().encode(memoText)); - }); - - it('should set memo from JSON object', function () { - const memoObj = { user: 'alice', amount: 1000 }; - transaction.setMemoData(memoObj); - - assert.strictEqual(transaction.hasMemo(), true); - assert.strictEqual(transaction.getMemoString(), JSON.stringify(memoObj)); - }); - - it('should set memo from bytes', function () { - const memoBytes = new TextEncoder().encode('Binary memo data'); - transaction.setMemoData(memoBytes); - - assert.strictEqual(transaction.hasMemo(), true); - assert.deepStrictEqual(transaction.getMemoBytes(), memoBytes); - assert.strictEqual(transaction.getMemoString(), 'Binary memo data'); - }); - - it('should handle empty string memo', function () { - transaction.setMemo(''); - assert.strictEqual(transaction.hasMemo(), false); - assert.strictEqual(transaction.getMemoString(), ''); - }); - - it('should handle UTF-8 memo', function () { - const utf8Memo = 'Hello δΈ–η•Œ 🌍 Flare!'; - transaction.setMemo(utf8Memo); - - assert.strictEqual(transaction.hasMemo(), true); - assert.strictEqual(transaction.getMemoString(), utf8Memo); - }); - - it('should include memo in transaction JSON when present', function () { - const memoText = 'Transaction metadata'; - transaction.setMemo(memoText); - - // Mock the FlareJS transaction - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - transaction.setTransactionType(TransactionType.Export); - - const txData = transaction.toJson(); - assert.strictEqual(txData.memo, memoText); - }); - - it('should not include memo in JSON when empty', function () { - // Mock the FlareJS transaction - const mockTx = createMockTx(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setTransaction(mockTx as any); - transaction.setTransactionType(TransactionType.Export); - - const txData = transaction.toJson(); - assert.strictEqual(txData.memo, ''); - }); - - it('should validate memo size limits', function () { - // Test large memo - this should be validated by utils - const largeMemo = 'x'.repeat(5000); // 5KB memo - transaction.setMemo(largeMemo); - assert.strictEqual(transaction.getMemoString(), largeMemo); - }); - - it('should handle special characters in memo', function () { - const specialMemo = 'Special chars: \n\t\r\0\x01\xff'; - transaction.setMemo(specialMemo); - assert.strictEqual(transaction.getMemoString(), specialMemo); - }); - - it('should throw error for invalid memo format in setMemoData', function () { - // This should be tested in utils, but we can check basic behavior - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transaction.setMemoData(123 as any); - }); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/utils.ts b/modules/sdk-coin-flrp/test/unit/lib/utils.ts deleted file mode 100644 index d262c7720d..0000000000 --- a/modules/sdk-coin-flrp/test/unit/lib/utils.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { coins, FlareNetwork } from '@bitgo/statics'; -import { NotImplementedError } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { Utils } from '../../../src/lib/utils'; -import { KeyPair } from '../../../src/lib'; -import * as testData from '../../resources/account'; - -describe('Utils', function () { - let utils: Utils; - const network = coins.get('tflrp').network as FlareNetwork; - - beforeEach(function () { - utils = new Utils(); - }); - - describe('includeIn', function () { - it('should return true when all wallet addresses are in output addresses', function () { - const walletAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address]; - const outputAddresses = [ - testData.ACCOUNT_1.addressMainnet, - testData.ACCOUNT_3.address, - testData.ACCOUNT_4.address, - ]; - - assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), true); - }); - - it('should return false when not all wallet addresses are in output addresses', function () { - const walletAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address]; - const outputAddresses = [testData.ACCOUNT_3.address, testData.ACCOUNT_4.address]; - - assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), false); - }); - - it('should return true for empty wallet addresses', function () { - const walletAddresses: string[] = []; - const outputAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address]; - - assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), true); - }); - - it('should return false when wallet address not found in empty output addresses', function () { - const walletAddresses = [testData.ACCOUNT_1.addressMainnet]; - const outputAddresses: string[] = []; - - assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), false); - }); - }); - - describe('isValidAddress', function () { - it('should validate single valid Flare addresses', function () { - const validAddresses = [ - testData.SEED_ACCOUNT.addressMainnet, - testData.SEED_ACCOUNT.addressTestnet, - testData.ACCOUNT_1.addressMainnet, - testData.ACCOUNT_1.addressTestnet, - ]; - - validAddresses.forEach((addr) => { - const result = utils.isValidAddress(addr); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - }); - - it('should validate array of addresses', function () { - const addresses = [ - testData.SEED_ACCOUNT.addressMainnet, - testData.SEED_ACCOUNT.addressTestnet, - testData.ACCOUNT_1.addressMainnet, - testData.ACCOUNT_1.addressTestnet, - ]; - - const result = utils.isValidAddress(addresses); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - - it('should validate addresses separated by ~', function () { - const addressString = - testData.SEED_ACCOUNT.addressTestnet + - '~' + - testData.ACCOUNT_1.addressTestnet + - '~' + - testData.ACCOUNT_4.address; - - const result = utils.isValidAddress(addressString); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - - it('should reject obviously invalid addresses', function () { - const invalidAddresses = [ - '', - 'invalid', - '123', - 'bitcoin1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq', - 'eth:0x1234567890123456789012345678901234567890', - ]; - - invalidAddresses.forEach((addr) => { - const result = utils.isValidAddress(addr); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, false); - }); - }); - }); - - describe('isValidAddressRegex', function () { - it('should test address format with regex', function () { - const testAddress = testData.SEED_ACCOUNT.addressTestnet; - const result = utils['isValidAddressRegex'](testAddress); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - - it('should reject empty strings', function () { - const result = utils['isValidAddressRegex'](''); - assert.strictEqual(result, false); - }); - }); - - describe('isValidTransactionId', function () { - it('should return true for valid transaction IDs', function () { - const validTxIds = [ - '6wewzpFrTDPGmFfRJoT9YyGVxsRDxQXu6pz6LSXLf2eU6StBe', - '3SuMRBREQwhsR1qQYjSpHPNgwV7keXQbKBgP8jULnKdz7ppEV', - '2ExGh7o1c4gQtQrzDt2BvJxg42FswGWaLY7NEXCqcejPxjSTij', - ]; - - validTxIds.forEach((txId) => { - const result = utils.isValidTransactionId(txId); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - }); - it('should return false for invalid transaction IDs', function () { - const invalidTxIds = [ - '', - '123', - 'invalidtxid', - '0xaf32fd2276be99560e5218d79f9c3d2f29c126fa61b60b08a42c1be430f877df', - ]; - - invalidTxIds.forEach((txId) => { - const result = utils.isValidTransactionId(txId); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, false); - }); - }); - }); - - describe('isValidBlockId', function () { - it('should return true for valid block IDs', function () { - const validTxIds = [ - 'mg3B2HsQ8Pqe63J2arXi6uD3wGJV1fgCNe5bRufDToAgVRVBp', - 'rVWodN2iTugUMckkgf8ntXcoyuduey24ZgXCMi66mrFegcV4R', - '2MrU9G74ra9QX99wQRxvKrbzV93i6Ua7KgHMETVMSYoJq2tb5g', - ]; - - validTxIds.forEach((txId) => { - const result = utils.isValidBlockId(txId); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, true); - }); - }); - it('should return false for invalid block IDs', function () { - const invalidTxIds = [ - '', - '123', - 'invalidtxid', - '0xa2379e3804e603357e3a670f2696852aae8ffe2f22a7b79f7fba86f78c8f3290', - ]; - - invalidTxIds.forEach((txId) => { - const result = utils.isValidBlockId(txId); - assert.strictEqual(typeof result, 'boolean'); - assert.strictEqual(result, false); - }); - }); - }); - - describe('isValidSignature', function () { - it('should throw NotImplementedError', function () { - assert.throws( - () => utils.isValidSignature('signature123'), - NotImplementedError, - 'isValidSignature not implemented' - ); - }); - }); - - describe('createSignature', function () { - it('should create signature using secp256k1', function () { - const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8'); - const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex'); - - const signature = utils.createSignature(network, message, privateKey).toString('hex'); - - assert.ok(signature.length > 0); - assert.strictEqual(signature, testData.SEED_ACCOUNT.signature); - }); - - it('should create different signatures for different messages', function () { - const message1 = Buffer.from('message 1', 'utf8'); - const message2 = Buffer.from('message 2', 'utf8'); - const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex'); - - const sig1 = utils.createSignature(network, message1, privateKey); - const sig2 = utils.createSignature(network, message2, privateKey); - - assert.notDeepStrictEqual(sig1, sig2); - }); - - it('should throw error for invalid private key', function () { - const message = Buffer.from('hello world', 'utf8'); - const invalidPrivateKey = Buffer.from('invalid', 'utf8'); - - assert.throws(() => utils.createSignature(network, message, invalidPrivateKey), /Failed to create signature/); - }); - }); - - describe('verifySignature', function () { - it('should verify valid signature', function () { - const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8'); - const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex'); - const signature = utils.createSignature(network, message, privateKey); - const publicKey = Buffer.from(testData.SEED_ACCOUNT.publicKey, 'hex'); // Compressed public key format - const isValid = utils.verifySignature(network, message, signature, publicKey); - assert.strictEqual(typeof isValid, 'boolean'); - assert.strictEqual(isValid, true); - }); - - it('should return false for invalid signature', function () { - const message = Buffer.from('hello world', 'utf8'); - const invalidSignature = Buffer.from('invalid signature', 'utf8'); - const publicKey = Buffer.from('02' + '0'.repeat(62), 'hex'); - const result = utils.verifySignature(network, message, invalidSignature, publicKey); - assert.strictEqual(result, false); - }); - }); - - describe('recoverySignature', function () { - it('should recover signature', () => { - const compressed = true; - const keyPair = new KeyPair({ prv: testData.SEED_ACCOUNT.flrpPrivateKey }); - const prv = keyPair.getPrivateKey(); - const pub = keyPair.getPublicKey({ compressed }); - const message = Buffer.from(testData.SEED_ACCOUNT.message, 'hex'); - const signature = utils.createSignature(network, message, prv!); - utils.recoverySignature(network, message, signature).should.deepEqual(pub); - }); - - it('should recover same public key for same message and signature', function () { - const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8'); - const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex'); - const signature = utils.createSignature(network, message, privateKey); - - const pubKey1 = utils.recoverySignature(network, message, signature); - const pubKey2 = utils.recoverySignature(network, message, signature); - - assert.deepStrictEqual(pubKey1, pubKey2); - }); - - it('should throw error for invalid signature', function () { - const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8'); - const invalidSignature = Buffer.from('invalid signature', 'utf8'); - - assert.throws(() => utils.recoverySignature(network, message, invalidSignature), /Failed to recover signature/); - }); - - it('should throw error for empty message', function () { - const message = Buffer.alloc(0); - const signature = Buffer.alloc(65); // Empty but valid length signature (65 bytes: 64 signature + 1 recovery param) - - assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/); - }); - }); - - describe('address parsing utilities', function () { - it('should handle address separator constants', function () { - const { ADDRESS_SEPARATOR } = require('../../../src/lib/iface'); - assert.strictEqual(ADDRESS_SEPARATOR, '~'); - }); - - it('should handle input separator constants', function () { - const { INPUT_SEPARATOR } = require('../../../src/lib/iface'); - assert.strictEqual(INPUT_SEPARATOR, ':'); - }); - }); - - describe('error handling', function () { - it('should properly extend base utils', function () { - // Test that utils class exists and has expected methods - assert.ok('isValidAddress' in utils); - assert.ok('includeIn' in utils); - assert.ok('createSignature' in utils); - assert.ok('verifySignature' in utils); - }); - - it('should handle parsing errors gracefully', function () { - // Test that utils can handle malformed input without crashing - // Note: These may throw errors, which is acceptable behavior - try { - utils.isValidAddress(null as unknown as string); - utils.isValidAddress(undefined as unknown as string); - } catch (error) { - // Expected behavior - utils should handle or throw meaningful errors - assert.ok(error instanceof Error); - } - }); - }); - - describe('constants validation', function () { - it('should have correct constant values', function () { - const constants = require('../../../src/lib/constants'); - - assert.strictEqual(typeof constants.DECODED_BLOCK_ID_LENGTH, 'number'); - assert.strictEqual(typeof constants.SHORT_PUB_KEY_LENGTH, 'number'); - assert.strictEqual(typeof constants.COMPRESSED_PUBLIC_KEY_LENGTH, 'number'); - assert.strictEqual(typeof constants.UNCOMPRESSED_PUBLIC_KEY_LENGTH, 'number'); - assert.strictEqual(typeof constants.RAW_PRIVATE_KEY_LENGTH, 'number'); - assert.strictEqual(typeof constants.SUFFIXED_PRIVATE_KEY_LENGTH, 'number'); - assert.strictEqual(typeof constants.PRIVATE_KEY_COMPRESSED_SUFFIX, 'string'); - assert.strictEqual(typeof constants.OUTPUT_INDEX_HEX_LENGTH, 'number'); - assert.ok(constants.ADDRESS_REGEX instanceof RegExp); - assert.ok(constants.HEX_REGEX instanceof RegExp); - }); - }); - - describe('Memo Utilities', function () { - it('should convert string to bytes', function () { - const text = 'Hello Flare'; - const bytes = utils.stringToBytes(text); - - assert.ok(bytes instanceof Uint8Array); - assert.strictEqual(utils.bytesToString(bytes), text); - }); - - it('should handle UTF-8 strings', function () { - const text = 'Hello δΈ–η•Œ 🌍'; - const bytes = utils.stringToBytes(text); - - assert.strictEqual(utils.bytesToString(bytes), text); - }); - - it('should create memo bytes from string', function () { - const text = 'Memo text'; - const bytes = utils.createMemoBytes(text); - - assert.ok(bytes instanceof Uint8Array); - assert.strictEqual(utils.parseMemoBytes(bytes), text); - }); - - it('should create memo bytes from JSON object', function () { - const obj = { type: 'payment', amount: 1000 }; - const bytes = utils.createMemoBytes(obj); - const parsed = utils.parseMemoBytes(bytes); - - assert.strictEqual(parsed, JSON.stringify(obj)); - }); - - it('should handle Uint8Array input', function () { - const originalBytes = new Uint8Array([1, 2, 3, 4]); - const bytes = utils.createMemoBytes(originalBytes); - - assert.deepStrictEqual(bytes, originalBytes); - }); - - it('should throw error for invalid memo type', function () { - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - utils.createMemoBytes(123 as any); - }, /Invalid memo format/); - }); - - it('should parse empty memo', function () { - const emptyBytes = new Uint8Array([]); - const parsed = utils.parseMemoBytes(emptyBytes); - - assert.strictEqual(parsed, ''); - }); - - it('should validate memo size', function () { - const smallMemo = new Uint8Array([1, 2, 3]); - const largeMemo = new Uint8Array(5000); - - assert.strictEqual(utils.validateMemoSize(smallMemo), true); - assert.strictEqual(utils.validateMemoSize(largeMemo), false); - assert.strictEqual(utils.validateMemoSize(largeMemo, 6000), true); - }); - - it('should handle special characters in memo', function () { - const specialText = 'Special: \n\t\r\0'; - const bytes = utils.createMemoBytes(specialText); - const parsed = utils.parseMemoBytes(bytes); - - assert.strictEqual(parsed, specialText); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/permissionlessValidatorTxBuilder.test.ts b/modules/sdk-coin-flrp/test/unit/permissionlessValidatorTxBuilder.test.ts deleted file mode 100644 index a7fe434a7d..0000000000 --- a/modules/sdk-coin-flrp/test/unit/permissionlessValidatorTxBuilder.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { PermissionlessValidatorTxBuilder } from '../../src/lib/permissionlessValidatorTxBuilder'; - -describe('PermissionlessValidatorTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: PermissionlessValidatorTxBuilder; - - beforeEach(function () { - builder = new PermissionlessValidatorTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should create a permissionless validator transaction builder', function () { - assert.ok(builder); - assert.ok(builder instanceof PermissionlessValidatorTxBuilder); - }); - - it('should set transaction type to AddPermissionlessValidator', function () { - assert.strictEqual(builder['transactionType'], TransactionType.AddPermissionlessValidator); - }); - - it('should initialize with default values', function () { - assert.strictEqual(builder['_nodeID'], undefined); - assert.strictEqual(builder['_blsPublicKey'], undefined); - assert.strictEqual(builder['_blsSignature'], undefined); - assert.strictEqual(builder['_startTime'], undefined); - assert.strictEqual(builder['_endTime'], undefined); - assert.strictEqual(builder['_stakeAmount'], undefined); - assert.strictEqual(builder['recoverSigner'], undefined); // AtomicTransactionBuilder doesn't inherit this - assert.strictEqual(builder['_delegationFeeRate'], undefined); - }); - }); - - describe('nodeID management', function () { - it('should set valid node ID', function () { - const nodeID = 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'; - const result = builder.nodeID(nodeID); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_nodeID'], nodeID); - }); - - it('should reject empty node ID', function () { - assert.throws(() => { - builder.nodeID(''); - }, BuildTransactionError); - }); - }); - - describe('BLS key management', function () { - it('should set valid BLS public key', function () { - const blsKey = '0x' + 'a'.repeat(96); // 48 bytes = 96 hex chars - const result = builder.blsPublicKey(blsKey); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_blsPublicKey'], blsKey); - }); - - it('should reject invalid BLS public key', function () { - assert.throws(() => { - builder.blsPublicKey('invalid-key'); - }, BuildTransactionError); - }); - - it('should set valid BLS signature', function () { - const blsSignature = '0x' + 'b'.repeat(192); // 96 bytes = 192 hex chars - const result = builder.blsSignature(blsSignature); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_blsSignature'], blsSignature); - }); - - it('should reject invalid BLS signature', function () { - assert.throws(() => { - builder.blsSignature('invalid-signature'); - }, BuildTransactionError); - }); - }); - - describe('time management', function () { - describe('startTime', function () { - it('should set valid start time with bigint', function () { - const time = 1640995200n; - const result = builder.startTime(time); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_startTime'], time); - }); - - it('should set valid start time with number', function () { - const time = 1640995200; - const result = builder.startTime(time); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_startTime'], BigInt(time)); - }); - - it('should reject negative start time', function () { - assert.throws(() => { - builder.startTime(-1n); - }, BuildTransactionError); - }); - }); - - describe('endTime', function () { - it('should set valid end time with bigint', function () { - const time = 1641081600n; - const result = builder.endTime(time); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_endTime'], time); - }); - - it('should set valid end time with number', function () { - const time = 1641081600; - const result = builder.endTime(time); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_endTime'], BigInt(time)); - }); - - it('should reject negative end time', function () { - assert.throws(() => { - builder.endTime(-1n); - }, BuildTransactionError); - }); - }); - }); - - describe('stake amount management', function () { - it('should set valid stake amount with bigint', function () { - const amount = 1000000000000000000n; - const result = builder.stakeAmount(amount); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_stakeAmount'], amount); - }); - - it('should set valid stake amount with number', function () { - const amount = 1000000; - const result = builder.stakeAmount(amount); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_stakeAmount'], BigInt(amount)); - }); - - it('should reject negative stake amount', function () { - assert.throws(() => { - builder.stakeAmount(-1n); - }, BuildTransactionError); - }); - }); - - describe('delegation fee rate management', function () { - it('should set valid delegation fee rate', function () { - const feeRate = 25000; // 2.5% in basis points - const result = builder.delegationFeeRate(feeRate); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], feeRate); - }); - - it('should reject delegation fee rate below minimum', function () { - // Test with a value below the expected minimum - assert.throws(() => { - builder.delegationFeeRate(10000); // 1% - should be too low - }, BuildTransactionError); - }); - }); - - describe('validation through methods', function () { - it('should validate through constructor and methods', function () { - // Test that the builder accepts valid values - assert.doesNotThrow(() => { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200n) - .endTime(1641081600n) - .stakeAmount(1000000000000000000n) - .delegationFeeRate(25000); - }); - }); - - it('should reject invalid values through methods', function () { - // Test that invalid values are rejected - assert.throws(() => { - builder.nodeID(''); - }, BuildTransactionError); - - assert.throws(() => { - builder.startTime(-1n); - }, BuildTransactionError); - - assert.throws(() => { - builder.endTime(-1n); - }, BuildTransactionError); - - assert.throws(() => { - builder.stakeAmount(-1n); - }, BuildTransactionError); - }); - }); - - describe('Method chaining', function () { - it('should support full method chaining', function () { - const blsKey = '0x' + 'a'.repeat(96); - const blsSignature = '0x' + 'b'.repeat(192); - - const result = builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .blsPublicKey(blsKey) - .blsSignature(blsSignature) - .startTime(1640995200n) - .endTime(1641081600n) - .stakeAmount(1000000000000000000n) - .delegationFeeRate(25000); - - assert.strictEqual(result, builder); - }); - - it('should support chaining in different order', function () { - const blsKey = '0x' + 'a'.repeat(96); - const blsSignature = '0x' + 'b'.repeat(192); - - const result = builder - .delegationFeeRate(30000) - .stakeAmount(2000000000000000000n) - .endTime(1641081600n) - .startTime(1640995200n) - .blsSignature(blsSignature) - .blsPublicKey(blsKey) - .nodeID('NodeID-8Yhx3nExvDS55k53UDC7V6680ftdSu4Mh'); - - assert.strictEqual(result, builder); - }); - }); - - describe('Edge cases and validation', function () { - it('should handle minimum valid values', function () { - const blsKey = '0x' + 'a'.repeat(96); - const blsSignature = '0x' + 'b'.repeat(192); - const minFee = 20000; // Default minimum delegation fee - - assert.doesNotThrow(() => { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .blsPublicKey(blsKey) - .blsSignature(blsSignature) - .startTime(0n) - .endTime(1n) - .stakeAmount(1n) - .delegationFeeRate(minFee); - }); - }); - - it('should maintain state correctly after multiple operations', function () { - const nodeID1 = 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'; - const nodeID2 = 'NodeID-8Yhx3nExvDS55k53UDC7V6680ftdSu4Mh'; - - builder.nodeID(nodeID1); - assert.strictEqual(builder['_nodeID'], nodeID1); - - builder.nodeID(nodeID2); - assert.strictEqual(builder['_nodeID'], nodeID2); - }); - - it('should handle BLS key format validation edge cases', function () { - // Too short - assert.throws(() => { - builder.blsPublicKey('0x' + 'a'.repeat(95)); - }, BuildTransactionError); - - // Too long - assert.throws(() => { - builder.blsPublicKey('0x' + 'a'.repeat(97)); - }, BuildTransactionError); - - // No 0x prefix - assert.throws(() => { - builder.blsPublicKey('a'.repeat(96)); - }, BuildTransactionError); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/smoke.ts b/modules/sdk-coin-flrp/test/unit/smoke.ts deleted file mode 100644 index 2bca880b26..0000000000 --- a/modules/sdk-coin-flrp/test/unit/smoke.ts +++ /dev/null @@ -1,23 +0,0 @@ -import 'mocha'; - -describe('sdk-coin-flrp smoke tests', function () { - it('should load all main modules without errors', function () { - // Test that main modules can be imported without throwing - (() => require('../../src/flrp')).should.not.throw(); - (() => require('../../src/lib/utils')).should.not.throw(); - (() => require('../../src/lib/atomicTransactionBuilder')).should.not.throw(); - (() => require('../../src/lib/iface')).should.not.throw(); - }); - - it('should have proper module exports', function () { - const flrp = require('../../src/flrp'); - const utils = require('../../src/lib/utils'); - const atomicTxBuilder = require('../../src/lib/atomicTransactionBuilder'); - const iface = require('../../src/lib/iface'); - - flrp.should.have.property('Flrp'); - utils.should.have.property('Utils'); - atomicTxBuilder.should.have.property('AtomicTransactionBuilder'); - iface.should.have.property('ADDRESS_SEPARATOR'); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/transactionBuilder.test.ts b/modules/sdk-coin-flrp/test/unit/transactionBuilder.test.ts deleted file mode 100644 index b311629799..0000000000 --- a/modules/sdk-coin-flrp/test/unit/transactionBuilder.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { TransactionBuilder } from '../../src/lib/transactionBuilder'; - -describe('TransactionBuilder', function () { - const coinConfig = coins.get('tflrp'); - - // We can't directly instantiate abstract TransactionBuilder, - // so we test through concrete implementations or static methods - describe('Class availability', function () { - it('should be available as a constructor', function () { - assert.ok(TransactionBuilder); - assert.ok(typeof TransactionBuilder === 'function'); - }); - }); - - describe('Validation methods', function () { - it('should have validateAmount method', function () { - // Test through a concrete implementation (we'll use DelegatorTxBuilder) - const { DelegatorTxBuilder } = require('../../src/lib/delegatorTxBuilder'); - const builder = new DelegatorTxBuilder(coinConfig); - - // Valid amount - assert.doesNotThrow(() => { - // Access protected method - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).validateAmount(1000000000000000000n); - }); - - // Invalid amount (negative) - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).validateAmount(-1000n); - }, BuildTransactionError); - - // Invalid amount (zero) - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).validateAmount(0n); - }, BuildTransactionError); - }); - - it('should validate node IDs through delegator builder', function () { - const { DelegatorTxBuilder } = require('../../src/lib/delegatorTxBuilder'); - const builder = new DelegatorTxBuilder(coinConfig); - - // Valid node ID - assert.doesNotThrow(() => { - builder.nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - }); - - // Invalid node ID (empty) - assert.throws(() => { - builder.nodeID(''); - }, BuildTransactionError); - }); - - it('should validate time values through delegator builder', function () { - const { DelegatorTxBuilder } = require('../../src/lib/delegatorTxBuilder'); - const builder = new DelegatorTxBuilder(coinConfig); - - // Valid start time - assert.doesNotThrow(() => { - builder.startTime(1640995200); - }); - - // Invalid start time (negative) - assert.throws(() => { - builder.startTime(-1); - }, BuildTransactionError); - }); - }); - - describe('Network and Blockchain validation', function () { - it('should validate through initBuilder method', function () { - const { DelegatorTxBuilder } = require('../../src/lib/delegatorTxBuilder'); - const builder = new DelegatorTxBuilder(coinConfig); - - // Test with valid transaction data (should not throw) - const validTx = { - nodeID: 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg', - startTime: 1640995200, - endTime: 1641081600, - stakeAmount: '1000000000000000000', - }; - - assert.doesNotThrow(() => { - builder.initBuilder(validTx); - }); - }); - }); -}); diff --git a/modules/sdk-coin-flrp/test/unit/validatorTxBuilder.test.ts b/modules/sdk-coin-flrp/test/unit/validatorTxBuilder.test.ts deleted file mode 100644 index 11e8807ce4..0000000000 --- a/modules/sdk-coin-flrp/test/unit/validatorTxBuilder.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; -import * as assert from 'assert'; -import { ValidatorTxBuilder } from '../../src/lib/validatorTxBuilder'; -import { DelegatorTxBuilder } from '../../src/lib/delegatorTxBuilder'; - -describe('ValidatorTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ValidatorTxBuilder; - - beforeEach(function () { - builder = new ValidatorTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ValidatorTxBuilder); - assert.ok(builder instanceof DelegatorTxBuilder); - }); - - it('should have correct transaction type', function () { - assert.strictEqual(builder['transactionType'], TransactionType.AddValidator); - }); - - it('should initialize with undefined delegation fee rate', function () { - assert.strictEqual(builder['_delegationFeeRate'], undefined); - }); - }); - - describe('delegationFeeRate', function () { - it('should set valid delegation fee rate', function () { - const result = builder.delegationFeeRate(25000); // 2.5% - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], 25000); - }); - - it('should reject negative delegation fee rate', function () { - assert.throws(() => { - builder.delegationFeeRate(-1000); - }, BuildTransactionError); - }); - - it('should reject delegation fee rate below minimum (2%)', function () { - assert.throws(() => { - builder.delegationFeeRate(19999); // Below 2% - }, BuildTransactionError); - }); - - it('should accept minimum delegation fee rate (2%)', function () { - assert.doesNotThrow(() => { - builder.delegationFeeRate(20000); // Exactly 2% - }); - }); - - it('should accept delegation fee rate above minimum', function () { - assert.doesNotThrow(() => { - builder.delegationFeeRate(30000); // 3% - }); - }); - - it('should handle maximum possible delegation fee rate', function () { - assert.doesNotThrow(() => { - builder.delegationFeeRate(1000000); // 100% - }); - }); - - it('should update delegation fee rate when called multiple times', function () { - builder.delegationFeeRate(25000); - assert.strictEqual(builder['_delegationFeeRate'], 25000); - - builder.delegationFeeRate(30000); - assert.strictEqual(builder['_delegationFeeRate'], 30000); - }); - }); - - describe('Method chaining', function () { - it('should allow method chaining with delegation fee rate', function () { - const result = builder - .delegationFeeRate(25000) // 2.5% - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000'); - - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], 25000); - assert.strictEqual(builder['_nodeID'], 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - assert.strictEqual(builder['_startTime'], 1640995200n); - assert.strictEqual(builder['_endTime'], 1641081600n); - assert.strictEqual(builder['_stakeAmount'], 1000000000000000000n); - }); - - it('should chain with all delegator methods', function () { - const result = builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000') - .delegationFeeRate(25000) // 2.5% - .rewardAddresses(['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']); - - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], 25000); - }); - - it('should work when delegation fee rate is set first', function () { - const result = builder - .delegationFeeRate(30000) // 3% - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], 30000); - assert.strictEqual(builder['_nodeID'], 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - }); - - it('should work when delegation fee rate is set last', function () { - const result = builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .delegationFeeRate(25000); // 2.5% - - assert.strictEqual(result, builder); - assert.strictEqual(builder['_delegationFeeRate'], 25000); - assert.strictEqual(builder['_nodeID'], 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - }); - }); - - describe('Inheritance from DelegatorTxBuilder', function () { - it('should inherit nodeID method', function () { - const result = builder.nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_nodeID'], 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - }); - - it('should inherit startTime method', function () { - const result = builder.startTime(1640995200); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_startTime'], 1640995200n); - }); - - it('should inherit endTime method', function () { - const result = builder.endTime(1641081600); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_endTime'], 1641081600n); - }); - - it('should inherit stakeAmount method', function () { - const result = builder.stakeAmount('1000000000000000000'); - assert.strictEqual(result, builder); - assert.strictEqual(builder['_stakeAmount'], 1000000000000000000n); - }); - - it('should inherit rewardAddresses method', function () { - const addresses = ['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']; - const result = builder.rewardAddresses(addresses); - assert.strictEqual(result, builder); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepStrictEqual((builder as any).transaction._rewardAddresses, addresses); - }); - - it('should inherit validation from delegator methods', function () { - // Should inherit node ID validation - empty node ID should throw - assert.throws(() => { - builder.nodeID(''); - }, BuildTransactionError); - - // Should inherit time validation - negative time should throw - assert.throws(() => { - builder.startTime(-1); - }, BuildTransactionError); - - // Should inherit stake amount validation - negative amount should throw - assert.throws(() => { - builder.stakeAmount(-1000); - }, BuildTransactionError); - }); - }); - - describe('Transaction building validation', function () { - it('should require delegation fee rate for validation', async function () { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000') - .rewardAddresses(['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']); - - // Validator builder should fail validation without delegation fee rate (0 is invalid) - await assert.rejects(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }, BuildTransactionError); - }); - - it('should pass validation with delegation fee rate', async function () { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000') - .delegationFeeRate(25000) // 2.5% - .rewardAddresses(['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']); - - await assert.doesNotReject(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }); - }); - - it('should require all delegator fields', async function () { - builder.delegationFeeRate(25000); // 2.5% - - // Should still fail without other required fields - await assert.rejects(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }, BuildTransactionError); - }); - }); - - describe('buildFlareTransaction', function () { - it('should reject building without delegation fee rate', async function () { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000') - .rewardAddresses(['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']); - - await assert.rejects(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }, BuildTransactionError); - }); - - it('should reject building without required delegator fields', async function () { - builder.delegationFeeRate(25000); // 2.5% - - await assert.rejects(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }, BuildTransactionError); - }); - - it('should build transaction with all required fields', async function () { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1640995200) - .endTime(1641081600) - .stakeAmount('1000000000000000000') - .delegationFeeRate(25000) // 2.5% - .rewardAddresses(['P-flare1x0r5h0l8w6nj3k2p0d5g9t5b8v3a9k1m']); - - await assert.doesNotReject(async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (builder as any).buildFlareTransaction(); - }); - }); - - it('should inherit time validation from delegator builder', function () { - // This test verifies that time validation happens immediately when setting values - assert.throws(() => { - builder - .nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg') - .startTime(1641081600) - .endTime(1640995200) // End time before start time - .stakeAmount('1000000000000000000') - .delegationFeeRate(25000); // 2.5% - }, BuildTransactionError); - }); - }); - - describe('Edge cases and validation', function () { - it('should handle minimum delegation fee rate (2%)', function () { - assert.doesNotThrow(() => { - builder.delegationFeeRate(20000); // 2% - }); - assert.strictEqual(builder['_delegationFeeRate'], 20000); - }); - - it('should handle higher delegation fee rates', function () { - assert.doesNotThrow(() => { - builder.delegationFeeRate(50000); // 5% - }); - assert.strictEqual(builder['_delegationFeeRate'], 50000); - }); - - it('should handle fee rate validation edge cases', function () { - // Just below minimum (2%) - assert.throws(() => builder.delegationFeeRate(19999), BuildTransactionError); - - // Just at minimum - assert.doesNotThrow(() => builder.delegationFeeRate(20000)); - }); - - it('should maintain validator-specific properties after operations', function () { - builder.delegationFeeRate(25000); // 2.5% - builder.nodeID('NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - - assert.strictEqual(builder['_delegationFeeRate'], 25000); - assert.strictEqual(builder['_nodeID'], 'NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg'); - }); - }); -}); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 963cfa578e..0bcbd3d571 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -9,13 +9,14 @@ export interface FlareNetwork extends BaseNetwork { batcherContractAddress?: string; forwarderFactoryAddress?: string; forwarderImplementationAddress?: string; - blockchainID?: string; - cChainBlockchainID?: string; - networkID?: number; - hrp?: string; - alias?: string; + blockchainID: string; + cChainBlockchainID: string; + networkID: number; + hrp: string; + alias: string; + assetId: string; vm?: string; - txFee?: string; + txFee: string; maxImportFee?: string; createSubnetTx?: string; createChainTx?: string; @@ -23,7 +24,7 @@ export interface FlareNetwork extends BaseNetwork { minConsumption?: string; maxConsumption?: string; maxSupply?: string; - minStake?: string; + minStake: string; minStakeDuration?: string; maxStakeDuration?: string; minDelegationStake?: string; @@ -1872,6 +1873,7 @@ class Somi extends Mainnet implements EthereumNetwork { } export class FlareP extends Mainnet implements FlareNetwork { + assetId = 'Flare'; name = 'FlareP'; family = CoinFamily.FLRP; explorerUrl = 'https://flarescan.com/blockchain/pvm/transactions/'; @@ -1907,6 +1909,7 @@ export class FlarePTestnet extends Testnet implements FlareNetwork { networkID = 114; hrp = 'costwo'; alias = 'P'; + assetId = 'fxMAKpBQQpFedrUhWMsDYfCUJxdUw4mneTczKBzNg3rc2JUub'; vm = 'platformvm'; txFee = '1000000'; // defaults maxImportFee = '10000000'; // defaults @@ -1923,7 +1926,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/'; @@ -1937,7 +1940,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/'; diff --git a/yarn.lock b/yarn.lock index bee715f768..75ab4b4a6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2701,10 +2701,10 @@ "@ethersproject/properties" "^5.8.0" "@ethersproject/strings" "^5.8.0" -"@flarenetwork/flarejs@4.1.0-rc0": - version "4.1.0-rc0" - resolved "https://registry.npmjs.org/@flarenetwork/flarejs/-/flarejs-4.1.0-rc0.tgz" - integrity sha512-XR07tUI+jVx6Km6SSRcl5FXWRuntPZJ6ov2TmWXZ9T6CDAvt69FZupnHRKjK0jA0Qmjzcbb8Q3xbRSLTlPUecQ== +"@flarenetwork/flarejs@4.1.1": + version "4.1.1" + resolved "https://registry.npmjs.org/@flarenetwork/flarejs/-/flarejs-4.1.1.tgz#5aac35a43431e9e08263094da48838e6159a69e7" + integrity sha512-XuzMROKI/4LfOWt2NY3suagmq0PjRbhyVaDznfVzTI0kRl/64xDc74kElusidewh55Y/5Ajrl1wBPrRhXG4fNQ== dependencies: "@noble/curves" "1.3.0" "@noble/hashes" "1.3.3" @@ -21256,7 +21256,7 @@ ws@^6.1.0: ws@~8.17.1: version "8.17.1" - resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" + resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== wsl-utils@^0.1.0: