diff --git a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts index 97abda8650..cecdea29da 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts @@ -35,7 +35,7 @@ import { FLARE_ATOMIC_PARSED_PREFIX, HEX_ENCODING, } from './constants'; -import { createFlexibleHexRegex } from './utils'; +import utils, { createFlexibleHexRegex } from './utils'; /** * Flare P-chain atomic transaction builder with FlareJS credential support. @@ -420,4 +420,58 @@ export abstract class AtomicTransactionBuilder { ); } } + + /** + * 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} + */ + threshold(value: number): this { + this.validateThreshold(value); + this.transaction._threshold = value; + return this; + } + + /** + * Validates the threshold + * @param threshold + */ + validateThreshold(threshold: number): void { + if (!threshold || threshold !== 2) { + throw new BuildTransactionError('Invalid transaction: threshold must be set to 2'); + } + } + + /** + * 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 + */ + // TODO: check the format of the public keys + fromPubKey(senderPubKey: string | string[]): this { + const pubKeys = senderPubKey instanceof Array ? senderPubKey : [senderPubKey]; + this.transaction._fromAddresses = pubKeys.map(utils.parseAddress); + return this; + } + + /** + * 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 + */ + locktime(value: number): this { + const locktime = BigInt(value); + this.validateLocktime(locktime); + this.transaction._locktime = locktime; + return this; + } + + /** + * Validates locktime + * @param locktime + */ + validateLocktime(locktime: bigint): void { + if (!locktime || locktime < 0n) { + throw new BuildTransactionError('Invalid transaction: locktime must be 0 or higher'); + } + } } diff --git a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts index dddb9fe41a..cc3be4633e 100644 --- a/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/exportInCTxBuilder.ts @@ -15,6 +15,7 @@ import { NETWORK_ID_PROP, BLOCKCHAIN_ID_PROP, } from './constants'; +import utils from './utils'; // Lightweight interface placeholders replacing Avalanche SDK transaction shapes interface FlareExportInputShape { @@ -95,8 +96,12 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder { * @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 + const pubKeys = pAddresses instanceof Array ? pAddresses : pAddresses.split('~'); + if (!utils.isValidAddress(pubKeys)) { + throw new BuildTransactionError('Invalid to address'); + } + + // TODO need to check if address should be string or Buffer this.transaction._to = pubKeys.map((a) => a.toString()); return this; } diff --git a/modules/sdk-coin-flrp/src/lib/index.ts b/modules/sdk-coin-flrp/src/lib/index.ts index 2ac8d8c7bf..c6e322b5de 100644 --- a/modules/sdk-coin-flrp/src/lib/index.ts +++ b/modules/sdk-coin-flrp/src/lib/index.ts @@ -5,6 +5,7 @@ export { KeyPair } from './keyPair'; export { Utils }; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { Transaction } from './transaction'; +export { TransactionBuilder } from './transactionBuilder'; export { AtomicTransactionBuilder } from './atomicTransactionBuilder'; export { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder'; export { ImportInCTxBuilder } from './importInCTxBuilder'; diff --git a/modules/sdk-coin-flrp/src/lib/keyPair.ts b/modules/sdk-coin-flrp/src/lib/keyPair.ts index 2ba161f21e..3d6e03abc6 100644 --- a/modules/sdk-coin-flrp/src/lib/keyPair.ts +++ b/modules/sdk-coin-flrp/src/lib/keyPair.ts @@ -124,7 +124,7 @@ 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(); diff --git a/modules/sdk-coin-flrp/src/lib/transaction.ts b/modules/sdk-coin-flrp/src/lib/transaction.ts index 838ec92e56..b39b8de266 100644 --- a/modules/sdk-coin-flrp/src/lib/transaction.ts +++ b/modules/sdk-coin-flrp/src/lib/transaction.ts @@ -1,4 +1,4 @@ -import { UnsignedTx, Credential } from '@flarenetwork/flarejs'; +import { UnsignedTx, Credential, utils as flrpUtils, pvmSerial, secp256k1 } from '@flarenetwork/flarejs'; import { BaseKey, BaseTransaction, @@ -23,10 +23,6 @@ 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, @@ -35,6 +31,42 @@ import { DESTINATION_CHAIN_FIELD, } from './constants'; +/** + * Checks if a signature is empty + * @param signature + * @returns {boolean} + */ +function isEmptySignature(signature: string): boolean { + return !!signature && utils.removeHexPrefix(signature).startsWith(''.padStart(90, '0')); +} + +interface CheckSignature { + (signature: string, addressHex: string): boolean; +} + +function generateSelectorSignature(signatures: string[]): CheckSignature { + if (signatures.length > 1 && signatures.every((sig) => isEmptySignature(sig))) { + // Look for address. + return function (sig, address): boolean { + try { + if (!isEmptySignature(sig)) { + return false; + } + if (sig.startsWith('0x')) sig = sig.substring(2); + const pub = sig.substring(90); + return pub === address; + } catch (e) { + return false; + } + }; + } else { + // Look for empty string + return function (sig, address): boolean { + return isEmptySignature(sig); + }; + } +} + /** * Flare P-chain transaction implementation using FlareJS * Based on AVAX transaction patterns adapted for Flare network @@ -52,19 +84,19 @@ export class Transaction extends BaseTransaction { public _stakeAmount: bigint; public _threshold = 2; public _locktime = BigInt(0); - public _fromAddresses: string[] = []; + public _fromAddresses: string[] = []; // TODO need to check for string or Uint8Array public _rewardAddresses: string[] = []; public _utxos: DecodedUtxoObj[] = []; public _to: string[]; public _fee: Partial = {}; public _blsPublicKey: string; public _blsSignature: string; - public _memo: Uint8Array = new Uint8Array(); // FlareJS memo field + public _memo: Uint8Array = new Uint8Array(); // FlareJS memo field // TODO need to check need for this constructor(coinConfig: Readonly) { super(coinConfig); this._network = coinConfig.network as FlareNetwork; - this._assetId = FLR_ASSET_ID; // Default FLR asset + this._assetId = this._network.assetId || FLR_ASSET_ID; this._blockchainID = this._network.blockchainID || ''; this._networkID = this._network.networkID || 0; } @@ -82,14 +114,13 @@ export class Transaction extends BaseTransaction { 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 { @@ -107,27 +138,42 @@ export class Transaction extends BaseTransaction { * @param {KeyPair} keyPair */ async sign(keyPair: KeyPair): Promise { - const prv = keyPair.getPrivateKey() as Buffer; - + const prv = keyPair.getPrivateKey() as Uint8Array; + const addressHex = keyPair.getAddressBuffer().toString('hex'); if (!prv) { throw new SigningError('Missing private key'); } - 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 - - throw new Error('FlareJS signing not yet implemented - placeholder'); + const unsignedTx = this._flareTransaction as UnsignedTx; + const unsignedBytes = unsignedTx.toBytes(); + + const publicKey = secp256k1.getPublicKey(prv); + if (unsignedTx.hasPubkey(publicKey)) { + const signature = await secp256k1.sign(unsignedBytes, prv); + let checkSign: CheckSignature | undefined = undefined; + unsignedTx.credentials.forEach((c, index) => { + if (checkSign === undefined) { + checkSign = generateSelectorSignature(c.getSignatures()); + } + let find = false; + c.getSignatures().forEach((sig, index) => { + if (checkSign && checkSign(sig, addressHex)) { + c.setSignature(index, signature); + find = true; + } + }); + if (!find) { + throw new SigningError( + `Private key cannot sign the transaction, address hex ${addressHex}, public key: ${publicKey}` + ); + } + }); + } } /** @@ -171,7 +217,7 @@ export class Transaction extends BaseTransaction { } toHexString(byteArray: Uint8Array): string { - return Buffer.from(byteArray).toString(HEX_ENCODING); + return flrpUtils.bufferToHex(Buffer.from(byteArray)); } /** @inheritdoc */ @@ -180,9 +226,8 @@ export class Transaction extends BaseTransaction { throw new InvalidTransactionError('Empty transaction data'); } - // TODO: Implement FlareJS transaction serialization - // For now, return placeholder - return FLARE_TX_HEX_PLACEHOLDER; + // TODO: verify and implement FlareJS transaction serialization + return this.toHexString(flrpUtils.addChecksum((this._flareTransaction as UnsignedTx).getSignedTx().toBytes())); } toJson(): TxData { @@ -222,6 +267,7 @@ export class Transaction extends BaseTransaction { TransactionType.AddDelegator, TransactionType.AddPermissionlessValidator, TransactionType.AddPermissionlessDelegator, + TransactionType.ImportToC, ]; if (!supportedTypes.includes(transactionType)) { @@ -239,9 +285,8 @@ export class Transaction extends BaseTransaction { throw new InvalidTransactionError('Empty transaction for signing'); } - // TODO: Implement FlareJS signable payload extraction - // For now, return placeholder - return Buffer.from(FLARE_SIGNABLE_PAYLOAD); + // TODO: verify and Implement FlareJS signable payload extraction + return utils.sha256((this._flareTransaction as UnsignedTx).toBytes()); } get id(): string { @@ -249,23 +294,21 @@ export class Transaction extends BaseTransaction { throw new InvalidTransactionError('Empty transaction for ID generation'); } - // TODO: Implement FlareJS transaction ID generation - // For now, return placeholder - return FLARE_TRANSACTION_ID_PLACEHOLDER; + // TODO: verify and Implement FlareJS transaction ID generation + 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) => + flrpUtils.format(this._network.alias, this._network.hrp, Buffer.from(a, 'hex')) + ); } get rewardAddresses(): string[] { - return this._rewardAddresses.map((address) => { - // TODO: Format addresses using FlareJS utilities - return address; - }); + return this._rewardAddresses.map((a) => + flrpUtils.format(this._network.alias, this._network.hrp, Buffer.from(a, 'hex')) + ); } /** @@ -284,8 +327,12 @@ export class Transaction extends BaseTransaction { // TODO: Extract validator 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.toString(), }, ]; case TransactionType.AddDelegator: @@ -293,8 +340,12 @@ export class Transaction extends BaseTransaction { // 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.AddPermissionlessDelegatorTx + ).subnetValidator.validator.nodeId.toString(), + value: ( + (this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessDelegatorTx + ).subnetValidator.validator.weight.toString(), }, ]; default: @@ -309,17 +360,29 @@ export class Transaction extends BaseTransaction { get changeOutputs(): Entry[] { // TODO: Extract change outputs from FlareJS transaction // For now, return empty array - return []; + return ( + (this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessValidatorTx + ).baseTx.outputs.map(utils.mapOutputToEntry(this._network)); } 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, - })); + let inputs; + switch (this.type) { + case TransactionType.AddPermissionlessValidator: + default: + inputs = ((this._flareTransaction as UnsignedTx).getTx() as pvmSerial.AddPermissionlessValidatorTx).baseTx + .inputs; + break; + } + return inputs.map((input) => { + return { + id: input.utxoID.txID.toString() + INPUT_SEPARATOR + input.utxoID.outputIdx.value(), + address: this.fromAddresses.sort().join(ADDRESS_SEPARATOR), + value: input.amount().toString(), + }; + }); } /** diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts index 906168ec19..f7a31ad754 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilder.ts @@ -1,4 +1,10 @@ -import { BaseTransactionBuilder, BuildTransactionError } from '@bitgo/sdk-core'; +import { + BaseTransactionBuilder, + BuildTransactionError, + TransactionType, + BaseKey, + BaseTransaction, +} from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { DecodedUtxoObj, Tx } from './iface'; import { KeyPair } from './keyPair'; @@ -147,6 +153,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { * 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 */ + // TODO: need to check for the address format fromPubKey(senderPubKey: string | string[]): this { const pubKeys = senderPubKey instanceof Array ? senderPubKey : [senderPubKey]; this._transaction._fromAddresses = pubKeys; // Store as strings directly @@ -165,12 +172,6 @@ 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 @@ -185,6 +186,37 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { } } + /** @inheritdoc */ + protected async buildImplementation(): Promise { + this.buildFlareTransaction(); + this.transaction.setTransactionType(this.transactionType); + if (this.hasSigner) { + this._signer.forEach((keyPair) => this.transaction.sign(keyPair)); + } + return this.transaction; + } + + protected abstract get transactionType(): TransactionType; + + /** + * Getter for know if build should sign + */ + get hasSigner(): boolean { + return this._signer !== undefined && this._signer.length > 0; + } + + /** + * Build the Flare transaction using FlareJS API + * @protected + */ + protected abstract buildFlareTransaction(): Promise | void; + + /** @inheritdoc */ + protected signImplementation({ key }: BaseKey): BaseTransaction { + this._signer.push(new KeyPair({ prv: key })); + return this.transaction; + } + /** * Get the transaction instance */ diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts index 73066ffc28..559c64c66c 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts @@ -1,51 +1,11 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { NotImplementedError, TransactionType } from '@bitgo/sdk-core'; import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; - -// Placeholder builders - basic implementations for testing -export class ExportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Export; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class ImportTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.Import; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class ValidatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddValidator; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} - -export class DelegatorTxBuilder extends AtomicTransactionBuilder { - protected get transactionType(): TransactionType { - return TransactionType.AddDelegator; - } - - constructor(coinConfig: Readonly) { - super(coinConfig); - // Don't throw error, allow placeholder functionality - } -} +import { ImportInCTxBuilder } from './importInCTxBuilder'; +import { ValidatorTxBuilder } from './validatorTxBuilder'; +import { PermissionlessValidatorTxBuilder } from './permissionlessValidatorTxBuilder'; +import { ExportInCTxBuilder } from './exportInCTxBuilder'; +import { ExportInPTxBuilder } from './exportInPTxBuilder'; +import { ImportInPTxBuilder } from './importInPTxBuilder'; /** * Factory for Flare P-chain transaction builders @@ -70,7 +30,7 @@ export class TransactionBuilderFactory { // Create a mock export builder for now // In the future, this will parse the transaction and determine the correct type - const builder = new ExportTxBuilder(this._coinConfig); + const builder = new ExportInPTxBuilder(this._coinConfig); // Initialize with the hex data (placeholder) builder.initBuilder({ txHex }); @@ -79,21 +39,56 @@ export class TransactionBuilderFactory { } /** - * Create a transaction builder for a specific type - * @param type - Transaction type + * Initialize Validator builder + * + * @returns {ValidatorTxBuilder} the builder initialized */ - getBuilder(type: TransactionType): AtomicTransactionBuilder { - switch (type) { - case TransactionType.Export: - return new ExportTxBuilder(this._coinConfig); - case TransactionType.Import: - return new ImportTxBuilder(this._coinConfig); - case TransactionType.AddValidator: - return new ValidatorTxBuilder(this._coinConfig); - case TransactionType.AddDelegator: - return new DelegatorTxBuilder(this._coinConfig); - default: - throw new NotImplementedError(`Transaction type ${type} not supported`); - } + getValidatorBuilder(): ValidatorTxBuilder { + return new ValidatorTxBuilder(this._coinConfig); + } + + /** + * Initialize Permissionless Validator builder + * + * @returns {PermissionlessValidatorTxBuilder} the builder initialized + */ + getPermissionlessValidatorTxBuilder(): PermissionlessValidatorTxBuilder { + return new PermissionlessValidatorTxBuilder(this._coinConfig); + } + + /** + * Export Cross chain transfer + * + * @returns {ExportInPTxBuilder} the builder initialized + */ + getExportBuilder(): ExportInPTxBuilder { + return new ExportInPTxBuilder(this._coinConfig); + } + + /** + * Import Cross chain transfer + * + * @returns {ImportInPTxBuilder} the builder initialized + */ + getImportBuilder(): ImportInPTxBuilder { + return new ImportInPTxBuilder(this._coinConfig); + } + + /** + * Import in C chain Cross chain transfer + * + * @returns {ImportInCTxBuilder} the builder initialized + */ + getImportInCBuilder(): ImportInCTxBuilder { + return new ImportInCTxBuilder(this._coinConfig); + } + + /** + * Export in C chain Cross chain transfer + * + * @returns {ExportInCTxBuilder} the builder initialized + */ + getExportInCBuilder(): ExportInCTxBuilder { + return new ExportInCTxBuilder(this._coinConfig); } } diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index b06b2608f0..8473c9506b 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -53,6 +53,28 @@ export class Utils implements BaseUtils { return `${prefix}-${bech32.encode(hrp, words)}`; }; + /** + * Convert a NodeID string to a Buffer. This is a Flare-specific implementation + * that doesn't rely on the Avalanche dependency. + * + * NodeIDs in Flare follow the format: NodeID- + * + * @param {string} nodeID - The NodeID string to convert + * @returns {Buffer} - The decoded NodeID as a Buffer + */ + // TODO add test cases for this method + public NodeIDStringToBuffer = (nodeID: string): Buffer => { + // Remove 'NodeID-' prefix if present + const cleanNodeID = nodeID.startsWith('NodeID-') ? nodeID.slice(7) : nodeID; + + try { + // Decode base58 string to buffer + return Buffer.from(bs58.decode(cleanNodeID)); + } catch (error) { + throw new Error(`Invalid NodeID format: ${error instanceof Error ? error.message : String(error)}`); + } + }; + public includeIn(walletAddresses: string[], otxoOutputAddresses: string[]): boolean { return walletAddresses.map((a) => otxoOutputAddresses.includes(a)).reduce((a, b) => a && b, true); } @@ -126,19 +148,31 @@ export class Utils implements BaseUtils { } } - public parseAddress = (address: string): Buffer => { - return this.stringToAddress(address); + // TODO add test cases for this method + public parseAddress = (address: string): string => { + return this.stringToAddress(address).toString(HEX_ENCODING); }; public stringToAddress = (address: string, hrp?: string): Buffer => { + // Handle hex addresses + if (address.startsWith('0x')) { + return Buffer.from(address.slice(2), 'hex'); + } + + // Handle raw hex without 0x prefix + if (/^[0-9a-fA-F]{40}$/.test(address)) { + return Buffer.from(address, 'hex'); + } + + // Handle Bech32 addresses const parts = address.trim().split('-'); if (parts.length < 2) { - throw new Error('Error - Valid address should include -'); + throw new Error('Error - Valid bech32 address should include -'); } const split = parts[1].lastIndexOf('1'); if (split < 0) { - throw new Error('Error - Valid address must include separator (1)'); + throw new Error('Error - Valid bech32 address must include separator (1)'); } const humanReadablePart = parts[1].slice(0, split); diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts b/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts new file mode 100644 index 0000000000..f84fe9cd0d --- /dev/null +++ b/modules/sdk-coin-flrp/test/resources/transactionData/exportInC.ts @@ -0,0 +1,24 @@ +// Test data for building export transactions with multiple P-addresses +export const EXPORT_IN_C = { + unsignedTxHex: + '0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000147c0b1f5d366ea8f1d0cd2ce108321d2be3b338600000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000009000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000008954270000000000000000000000020000000320829837bfba5d602b19cb9102b99eb3f895d5e47c71b9ae100e813e6332eddad2554ec12a0591fcbb6de9adcfbf2e0dfeffbe7792afd0c4085fdd370000000100000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000accf9fe1', + fullsigntxHex: + '0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000147c0b1f5d366ea8f1d0cd2ce108321d2be3b338600000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000009000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000008954270000000000000000000000020000000320829837bfba5d602b19cb9102b99eb3f895d5e47c71b9ae100e813e6332eddad2554ec12a0591fcbb6de9adcfbf2e0dfeffbe7792afd0c4085fdd37000000010000000900000001175782fa48b5b308d8f378832e5e637febb1fbcbec8f75fd6a65b657418575777b45c17a01b548a559d99e1a570d1efebe9b595dca383b0e39d5a961f16dd27500bdc89d2b', + txhash: 'jHRxuZjnSHYNwWpUUyob7RpfHwj1wfuQa8DGWQrkDh2RQ5Jb3', + + xPrivateKey: + 'xprv9s21ZrQH143K2DW9jvDoAkVpRKi5V9XhZaVdoUcqoYPPQ9wRrLNT6VGgWBbRoSYB39Lak6kXgdTM9T3QokEi5n2JJ8EdggHLkZPX8eDiBu1', + privateKey: 'a533d8419d4518e11cd8d9f049c73a8bdaf003d6602319f967ce3c243e646ba5', + amount: '8999975', + cHexAddress: '0x47c0b1f5d366ea8f1d0cd2ce108321d2be3b3386', + pAddresses: [ + 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + 'P-costwo1n4a86kc3td6nvmwm4xh0w78mc5jjxc9g8w6en0', + 'P-costwo1nhm2vw8653f3qwtj3kl6qa359kkt6y9r7qgljv', + ], + mainAddress: 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + targetChainId: '11111111111111111111111111111111LpoYY', + nonce: 9, + threshold: 2, + fee: '25', +}; diff --git a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts index 7d958a8b07..a2c420cc16 100644 --- a/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts +++ b/modules/sdk-coin-flrp/test/unit/lib/exportInCTxBuilder.ts @@ -1,658 +1,105 @@ import { coins } from '@bitgo/statics'; import { BuildTransactionError } from '@bitgo/sdk-core'; import * as assert from 'assert'; -import { ExportInCTxBuilder } from '../../../src/lib/exportInCTxBuilder'; +import { TransactionBuilderFactory } from '../../../src/lib/transactionBuilderFactory'; +import { EXPORT_IN_C as testData } from '../../resources/transactionData/exportInC'; describe('ExportInCTxBuilder', function () { - const coinConfig = coins.get('tflrp'); - let builder: ExportInCTxBuilder; + const factory = new TransactionBuilderFactory(coins.get('tflrp')); + const txBuilder = factory.getExportInCBuilder(); - beforeEach(function () { - builder = new ExportInCTxBuilder(coinConfig); - }); - - describe('Constructor', function () { - it('should initialize with coin config', function () { - assert.ok(builder); - assert.ok(builder instanceof ExportInCTxBuilder); - }); - - it('should extend AtomicInCTransactionBuilder', function () { - // Test inheritance - assert.ok(builder); - }); - }); - - describe('UTXO Override', function () { - it('should throw error when trying to set UTXOs', function () { - const mockUtxos = [{ id: 'test' }]; - - assert.throws( - () => { - builder.utxos(mockUtxos); - }, - BuildTransactionError, - 'Should reject UTXOs for C-chain export transactions' - ); - }); - - it('should throw error for empty UTXO array', function () { - assert.throws( - () => { - builder.utxos([]); - }, - BuildTransactionError, - 'Should reject empty UTXO array' - ); - }); - - it('should throw error for any UTXO input', function () { - const testCases = [[], [{}], ['invalid'], null, undefined]; - - testCases.forEach((testCase, index) => { - assert.throws( - () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - builder.utxos(testCase as any); - }, - BuildTransactionError, - `Test case ${index} should throw error` - ); - }); + describe('utxos', function () { + it('should throw an error when utxos are used', async function () { + assert.throws(() => { + txBuilder.utxos([]); + }, new BuildTransactionError('utxos are not required in Export Tx in C-Chain')); }); }); - describe('Amount Management', function () { - it('should set valid positive amounts', function () { - const validAmounts = ['1000', '1000000000000000000', '999999999999999999']; + describe('amount', function () { + it('should accept valid amounts in different formats', function () { + const validAmounts = [BigInt(1000), '1000', 1000]; validAmounts.forEach((amount) => { assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept amount: ${amount}`); - }); - }); - - it('should set bigint amounts', function () { - const bigintAmounts = [1000n, 1000000000000000000n, 999999999999999999n]; - - bigintAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept bigint amount: ${amount}`); - }); - }); - - it('should set numeric amounts', function () { - const numericAmounts = [1000, 2000000, 999999999]; - - numericAmounts.forEach((amount) => { - assert.doesNotThrow(() => { - builder.amount(amount); - }, `Should accept numeric amount: ${amount}`); - }); - }); - - 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', function () { + it('should accept valid nonces in different formats', function () { + const validNonces = [BigInt(1), '1', 1, 0]; validNonces.forEach((nonce) => { assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept nonce: ${nonce}`); - }); - }); - - it('should set string nonce values', function () { - const stringNonces = ['0', '1', '1000', '999999999999']; - - stringNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept string nonce: ${nonce}`); - }); - }); - - it('should set numeric nonce values', function () { - const numericNonces = [0, 1, 1000, 999999]; - - numericNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should accept numeric nonce: ${nonce}`); - }); - }); - - it('should reject negative nonce values', function () { - const negativeNonces = [-1n, -1000n]; - - negativeNonces.forEach((nonce) => { - assert.throws( - () => { - builder.nonce(nonce); - }, - BuildTransactionError, - `Should reject negative nonce: ${nonce}` - ); - }); - }); - - it('should reject negative string nonce values', function () { - const negativeStringNonces = ['-1', '-1000']; - - negativeStringNonces.forEach((nonce) => { - assert.throws( - () => { - builder.nonce(nonce); - }, - BuildTransactionError, - `Should reject negative string nonce: ${nonce}` - ); - }); - }); - - it('should handle large nonce values', function () { - const largeNonces = [ - '18446744073709551615', // Max uint64 - BigInt('999999999999999999999999999999'), - 1000000000000000000n, - ]; - - largeNonces.forEach((nonce) => { - assert.doesNotThrow(() => { - builder.nonce(nonce); - }, `Should handle large nonce: ${nonce}`); - }); - }); - - it('should chain nonce setting with other methods', function () { - const nonce = 123n; - const amount = '1000000000000000000'; - - assert.doesNotThrow(() => { - builder.nonce(nonce).amount(amount); - }); - }); - }); - - describe('Destination Address Management', function () { - it('should set single destination address', function () { - const singleAddress = 'P-flare1destination'; - - assert.doesNotThrow(() => { - builder.to(singleAddress); - }); - }); - - it('should set multiple destination addresses', function () { - const multipleAddresses = ['P-flare1dest1', 'P-flare1dest2', 'P-flare1dest3']; - - assert.doesNotThrow(() => { - builder.to(multipleAddresses); - }); - }); - - it('should handle comma-separated addresses', function () { - const commaSeparated = 'P-flare1dest1~P-flare1dest2~P-flare1dest3'; - - assert.doesNotThrow(() => { - builder.to(commaSeparated); + txBuilder.nonce(nonce); + }); }); }); - it('should handle empty address array', function () { - assert.doesNotThrow(() => { - builder.to([]); - }); - }); - - it('should chain address setting with other methods', function () { - const addresses = ['P-flare1dest1', 'P-flare1dest2']; - const amount = '1000000000000000000'; - - assert.doesNotThrow(() => { - builder.to(addresses).amount(amount); - }); - }); - }); - - describe('Transaction Type Verification', function () { - it('should verify transaction type (placeholder returns true)', function () { - const mockTx = { type: 'export' }; - const result = ExportInCTxBuilder.verifyTxType(mockTx); - assert.strictEqual(result, true); // Placeholder always returns true - }); - - it('should handle different transaction objects', function () { - const testCases = [{}, null, undefined, { type: 'import' }, { data: 'test' }]; - - testCases.forEach((testCase, index) => { - const result = ExportInCTxBuilder.verifyTxType(testCase); - assert.strictEqual(result, true, `Test case ${index} should return true (placeholder)`); - }); - }); - - it('should verify via instance method', function () { - const mockTx = { type: 'export' }; - const result = builder.verifyTxType(mockTx); - assert.strictEqual(result, true); - }); - }); - - describe('Transaction Building', function () { - it('should handle building when transaction has credentials', function () { - const mockTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - builder.initBuilder(mockTx); - - // Should not throw when credentials exist - assert.doesNotThrow(() => { - // Access protected method via type assertion - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).buildFlareTransaction(); - }); - }); - - it('should require amount for building', function () { - builder.nonce(1n); - builder.to(['P-flare1dest']); - - // Mock setting from addresses via transaction initialization - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - builder.initBuilder(mockRawTx); - // Clear amount to test error - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any)._amount = undefined; - - 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); - }); - }); - - it('should validate blockchain ID during initialization', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.from('wrong-blockchain'), - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, Error); - }); - - it('should validate single output requirement', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default // Will match default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest1'], - amount: 500000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - { - addresses: ['P-flare1dest2'], - amount: 500000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - + it('should throw error for negative nonce', function () { assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); - }); - - it('should validate single input requirement', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test1', - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - { - address: 'C-flare1test2', - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 2n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); - }); - - it('should validate negative fee calculation', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 500000000000000000n, // Less than output - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, // More than input - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; - - assert.throws(() => { - builder.initBuilder(mockRawTx); - }, BuildTransactionError); + txBuilder.nonce('-1'); + }, new BuildTransactionError('Nonce must be greater or equal than 0')); }); }); - 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); - }); - }); + describe('to', function () { + const txBuilder = factory.getExportInCBuilder(); - it('should handle object raw transaction', function () { - const mockRawTx = { - unsignedTx: { - networkId: 0, // Match builder's default - sourceBlockchainId: Buffer.alloc(0), // Match builder's default - destinationBlockchainId: Buffer.from('test-dest'), - inputs: [ - { - address: 'C-flare1test', - amount: 2000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - nonce: 1n, - }, - ], - outputs: [ - { - addresses: ['P-flare1dest'], - amount: 1000000000000000000n, - assetId: Buffer.alloc(0), // Match builder's default - }, - ], - }, - credentials: [], - }; + it('should accept multiple P-addresses', function () { + const pAddresses = testData.pAddresses; assert.doesNotThrow(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (builder as any).fromImplementation(mockRawTx); + txBuilder.to(pAddresses); }); }); - }); - - describe('Integration Tests', function () { - it('should handle complete export flow preparation', function () { - const amount = '1000000000000000000'; // 1 FLR - const nonce = 123n; - const toAddresses = ['P-flare1destination']; - - // Complete setup - builder.amount(amount).nonce(nonce).to(toAddresses); - - // All operations should complete without throwing - assert.ok(true); - }); - it('should handle method chaining extensively', function () { - // Test extensive method chaining + it('should accept single P-address', function () { assert.doesNotThrow(() => { - builder - .amount('5000000000000000000') // 5 FLR - .nonce(456n) - .to(['P-flare1receiver1', 'P-flare1receiver2']); + txBuilder.to(testData.pAddresses[0]); }); }); - it('should handle large transaction values', function () { - const largeAmount = '100000000000000000000000'; // 100k FLR - const largeNonce = 999999999999n; + it('should accept tilde-separated P-addresses string', function () { + const pAddresses = testData.pAddresses.join('~'); 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); + txBuilder.to(pAddresses); }); }); }); - describe('Edge Cases', function () { - it('should handle zero values appropriately', function () { - // Zero amount should be rejected - assert.throws(() => { - builder.amount(0); - }, /Amount must be positive/); - - // Zero nonce should be valid - assert.doesNotThrow(() => { - builder.nonce(0n); - }); - }); - - it('should handle maximum values', function () { - const maxBigInt = BigInt('18446744073709551615'); // Max uint64 - - assert.doesNotThrow(() => { - builder.amount(maxBigInt); - }); - - assert.doesNotThrow(() => { - builder.nonce(maxBigInt); - }); - }); - - it('should maintain state across multiple operations', function () { - // Build state incrementally - builder.amount('1000000000000000000'); - builder.nonce(123n); - builder.to(['P-flare1dest']); - - // State should be maintained across operations - assert.ok(true); + describe('should build a export txn from C to P', () => { + const newTxBuilder = () => + factory + .getExportInCBuilder() + .fromPubKey(testData.cHexAddress) + .nonce(testData.nonce) + .amount(testData.amount) + .threshold(testData.threshold) + .locktime(10) + .to(testData.pAddresses) + .feeRate(testData.fee); + + it('Should create export tx for same values', async () => { + const txBuilder = newTxBuilder(); + + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(testData.unsignedTxHex); }); }); }); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index b87879d436..d5ef5b8b6d 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -9,13 +9,13 @@ export interface FlareNetwork extends BaseNetwork { batcherContractAddress?: string; forwarderFactoryAddress?: string; forwarderImplementationAddress?: string; - blockchainID?: string; - cChainBlockchainID?: string; + blockchainID: string; + cChainBlockchainID: string; networkID?: number; - hrp?: string; - alias?: string; + hrp: string; + alias: string; vm?: string; - txFee?: string; + txFee: string; maxImportFee?: string; createSubnetTx?: string; createChainTx?: string; @@ -28,6 +28,7 @@ export interface FlareNetwork extends BaseNetwork { maxStakeDuration?: string; minDelegationStake?: string; minDelegationFee?: string; + assetId?: string; } import { CoinFamily } from './base'; @@ -1789,6 +1790,7 @@ export class FlareP extends Mainnet implements FlareNetwork { maxStakeDuration = '31536000'; // 1 year minDelegationStake = '50000000000000'; // 50000 FLR minDelegationFee = '0'; + assetId = 'FLRP'; } export class FlarePTestnet extends Testnet implements FlareNetwork { @@ -1815,9 +1817,10 @@ export class FlarePTestnet extends Testnet implements FlareNetwork { maxStakeDuration = '31536000'; // 1 year minDelegationStake = '50000000000000'; // 50000 FLR minDelegationFee = '0'; + assetId = 'FLRP'; } -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/'; @@ -1831,7 +1834,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/';