diff --git a/modules/sdk-coin-iota/src/iota.ts b/modules/sdk-coin-iota/src/iota.ts index 48523f17c9..a0d417e332 100644 --- a/modules/sdk-coin-iota/src/iota.ts +++ b/modules/sdk-coin-iota/src/iota.ts @@ -31,6 +31,10 @@ import { } from './lib/iface'; import { TransferTransaction } from './lib/transferTransaction'; +/** + * IOTA coin implementation. + * Supports TSS (Threshold Signature Scheme) with EDDSA algorithm. + */ export class Iota extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -44,15 +48,22 @@ export class Iota extends BaseCoin { this._staticsCoin = staticsCoin; } + /** + * Factory method to create an IOTA coin instance. + */ static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { return new Iota(bitgo, staticsCoin); } + // ======================================== + // Coin Configuration Methods + // ======================================== + getBaseFactor(): string | number { return Math.pow(10, this._staticsCoin.decimalPlaces); } - getChain() { + getChain(): string { return this._staticsCoin.name; } @@ -60,16 +71,18 @@ export class Iota extends BaseCoin { return this._staticsCoin.family; } - getFullName() { + getFullName(): string { return this._staticsCoin.fullName; } - /** @inheritDoc */ + // ======================================== + // Multi-Signature and TSS Support + // ======================================== + supportsTss(): boolean { return true; } - /** inherited doc */ getDefaultMultisigType(): MultisigType { return multisigTypes.tss; } @@ -78,74 +91,32 @@ export class Iota extends BaseCoin { return MPCType.EDDSA; } + // ======================================== + // Address and Public Key Validation + // ======================================== + /** - * Check if an address is valid - * @param address the address to be validated + * Validates an IOTA address. + * @param address - The address to validate (64-character hex string) * @returns true if the address is valid */ isValidAddress(address: string): boolean { - // IOTA addresses are 64-character hex strings return utils.isValidAddress(address); } /** - * @inheritDoc + * Validates a public key. + * @param pub - The public key to validate + * @returns true if the public key is valid */ - async explainTransaction(params: ExplainTransactionOptions): Promise { - const rawTx = params.txHex; - if (!rawTx) { - throw new Error('missing required tx prebuild property txHex'); - } - const transaction = await this.rebuildTransaction(rawTx); - if (!transaction) { - throw new Error('failed to explain transaction'); - } - return transaction.explainTransaction(); - } - - /** - * Verifies that a transaction prebuild complies with the original intention - * @param params - */ - async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txPrebuild: txPrebuild, txParams: txParams } = params; - const rawTx = txPrebuild.txHex; - if (!rawTx) { - throw new Error('missing required tx prebuild property txHex'); - } - const transaction = await this.rebuildTransaction(rawTx); - if (!transaction) { - throw new Error('failed to verify transaction'); - } - if (txParams.recipients !== undefined) { - if (!(transaction instanceof TransferTransaction)) { - throw new Error('Tx not a transfer transaction'); - } - const txData = transaction.toJson() as TransferTxData; - - if (!txData.recipients) { - throw new Error('Tx recipients does not match with expected txParams recipients'); - } - - const normalizeRecipient = (recipient: TransactionRecipient) => - _.pick(recipient, ['address', 'amount', 'tokenName']); - const txDataRecipients = txData.recipients.map(normalizeRecipient); - const txParamsRecipients = txParams.recipients.map(normalizeRecipient); - - const allRecipientsMatch = txParamsRecipients.every((expectedRecipient) => - txDataRecipients.some((actualRecipient) => _.isEqual(expectedRecipient, actualRecipient)) - ); - - if (!allRecipientsMatch) { - throw new Error('Tx recipients does not match with expected txParams recipients'); - } - } - return true; + isValidPub(pub: string): boolean { + return utils.isValidPublicKey(pub); } /** - * Check if an address belongs to a wallet - * @param params + * Verifies if an address belongs to a TSS wallet. + * @param params - Verification parameters including wallet address and user/backup public keys + * @returns true if the address belongs to the wallet */ async isWalletAddress(params: TssVerifyAddressOptions): Promise { return verifyEddsaTssWalletAddress( @@ -155,76 +126,81 @@ export class Iota extends BaseCoin { ); } + // ======================================== + // Transaction Explanation and Verification + // ======================================== + /** - * Parse a transaction - * @param params + * Explains a transaction by parsing its hex representation. + * @param params - Parameters containing the transaction hex + * @returns Detailed explanation of the transaction + * @throws Error if txHex is missing or transaction cannot be explained */ - async parseTransaction(params: IotaParseTransactionOptions): Promise { - const transactionExplanation = await this.explainTransaction({ txHex: params.txHex }); + async explainTransaction(params: ExplainTransactionOptions): Promise { + const rawTx = this.validateAndExtractTxHex(params.txHex, 'explain'); + const transaction = await this.rebuildTransaction(rawTx); + return transaction.explainTransaction(); + } - if (!transactionExplanation) { - throw new Error('Invalid transaction'); - } + /** + * Verifies that a transaction prebuild matches the original transaction parameters. + * Ensures recipients and amounts align with the intended transaction. + * + * @param params - Verification parameters containing prebuild and original params + * @returns true if verification succeeds + * @throws Error if verification fails + */ + async verifyTransaction(params: VerifyTransactionOptions): Promise { + const { txPrebuild, txParams } = params; + const rawTx = this.validateAndExtractTxHex(txPrebuild.txHex, 'verify'); - let fee = new BigNumber(0); + const transaction = await this.rebuildTransaction(rawTx); + this.validateTransactionType(transaction); - if (transactionExplanation.outputs.length <= 0) { - return { - inputs: [], - outputs: [], - fee, - }; + if (txParams.recipients !== undefined) { + this.verifyTransactionRecipients(transaction as TransferTransaction, txParams.recipients); } - const senderAddress = transactionExplanation.outputs[0].address; - if (transactionExplanation.fee.fee !== '') { - fee = new BigNumber(transactionExplanation.fee.fee); - } + return true; + } - const outputAmount = transactionExplanation.sponsor - ? new BigNumber(transactionExplanation.outputAmount).toFixed() - : new BigNumber(transactionExplanation.outputAmount).plus(fee).toFixed(); // assume 1 sender, who is also the fee payer + /** + * Parses a transaction and extracts inputs, outputs, and fees. + * @param params - Parameters containing the transaction hex + * @returns Parsed transaction with inputs, outputs, and fee information + */ + async parseTransaction(params: IotaParseTransactionOptions): Promise { + const transactionExplanation = await this.explainTransaction({ txHex: params.txHex }); - const inputs = [ - { - address: senderAddress, - amount: outputAmount, - }, - ]; - if (transactionExplanation.sponsor) { - inputs.push({ - address: transactionExplanation.sponsor, - amount: fee.toFixed(), - }); + if (!transactionExplanation || transactionExplanation.outputs.length === 0) { + return this.createEmptyParsedTransaction(); } - const outputs: { - address: string; - amount: string; - }[] = transactionExplanation.outputs.map((output) => { - return { - address: output.address, - amount: new BigNumber(output.amount).toFixed(), - }; - }); + const fee = this.calculateTransactionFee(transactionExplanation); + const inputs = this.buildTransactionInputs(transactionExplanation, fee); + const outputs = this.buildTransactionOutputs(transactionExplanation); - return { - inputs, - outputs, - fee, - }; + return { inputs, outputs, fee }; } + // ======================================== + // Key Generation and Signing + // ======================================== + /** - * Generate a key pair - * @param seed Optional seed to generate key pair from + * Generates a key pair for IOTA transactions. + * @param seed - Optional seed to generate deterministic key pair + * @returns Key pair with public and private keys + * @throws Error if private key generation fails */ generateKeyPair(seed?: Buffer): KeyPair { const keyPair = seed ? new IotaKeyPair({ seed }) : new IotaKeyPair(); const keys = keyPair.getKeys(); + if (!keys.prv) { throw new Error('Missing prv in key generation.'); } + return { pub: keys.pub, prv: keys.prv, @@ -232,24 +208,17 @@ export class Iota extends BaseCoin { } /** - * Check if a public key is valid - * @param pub Public key to check - */ - isValidPub(pub: string): boolean { - return utils.isValidPublicKey(pub); - } - - /** - * Sign a transaction - * @param params + * Signs a transaction (not implemented for IOTA). + * IOTA transactions are signed externally using TSS. */ async signTransaction(params: SignTransactionOptions): Promise { throw new Error('Method not implemented.'); } /** - * Audit a decrypted private key to ensure it's valid - * @param params + * Audits a decrypted private key to ensure it's valid for the given public key. + * @param params - Parameters containing multiSigType, private key, and public key + * @throws Error if multiSigType is not TSS or if key validation fails */ auditDecryptedKey({ multiSigType, prv, publicKey }: AuditDecryptedKeyParams): void { if (multiSigType !== multisigTypes.tss) { @@ -258,28 +227,199 @@ export class Iota extends BaseCoin { auditEddsaPrivateKey(prv, publicKey ?? ''); } - /** @inheritDoc */ + /** + * Extracts the signable payload from a serialized transaction. + * @param serializedTx - The serialized transaction hex + * @returns Buffer containing the signable payload + */ async getSignablePayload(serializedTx: string): Promise { const rebuiltTransaction = await this.rebuildTransaction(serializedTx); return rebuiltTransaction.signablePayload; } - /** inherited doc */ + /** + * Sets coin-specific fields in the transaction intent. + * @param intent - The populated intent object to modify + * @param params - Parameters containing unspents data + */ setCoinSpecificFieldsInIntent(intent: PopulatedIntent, params: PrebuildTransactionWithIntentOptions): void { intent.unspents = params.unspents; } + // ======================================== + // Private Helper Methods + // ======================================== + + /** + * Validates and extracts transaction hex from parameters. + * @param txHex - The transaction hex to validate + * @param operation - The operation being performed (for error messages) + * @returns The validated transaction hex + * @throws Error if txHex is missing + */ + private validateAndExtractTxHex(txHex: string | undefined, operation: string): string { + if (!txHex) { + throw new Error(`missing required tx prebuild property txHex for ${operation} operation`); + } + return txHex; + } + + /** + * Validates that the transaction is a TransferTransaction. + * @param transaction - The transaction to validate + * @throws Error if transaction is not a TransferTransaction + */ + private validateTransactionType(transaction: Transaction): void { + if (!(transaction instanceof TransferTransaction)) { + throw new Error('Tx not a transfer transaction'); + } + } + + /** + * Verifies that transaction recipients match the expected recipients. + * @param transaction - The transfer transaction to verify + * @param expectedRecipients - The expected recipients from transaction params + * @throws Error if recipients don't match + */ + private verifyTransactionRecipients( + transaction: TransferTransaction, + expectedRecipients: TransactionRecipient[] + ): void { + const txData = transaction.toJson() as TransferTxData; + + if (!txData.recipients) { + throw new Error('Tx recipients does not match with expected txParams recipients'); + } + + const actualRecipients = this.normalizeRecipients(txData.recipients); + const expected = this.normalizeRecipients(expectedRecipients); + + if (!this.recipientsMatch(expected, actualRecipients)) { + throw new Error('Tx recipients does not match with expected txParams recipients'); + } + } + + /** + * Normalizes recipients by extracting only relevant fields. + * @param recipients - Recipients to normalize + * @returns Normalized recipients with address, amount, and tokenName only + */ + private normalizeRecipients(recipients: TransactionRecipient[]): TransactionRecipient[] { + return recipients.map((recipient) => _.pick(recipient, ['address', 'amount', 'tokenName'])); + } + + /** + * Checks if expected recipients match actual recipients. + * @param expected - Expected recipients + * @param actual - Actual recipients from transaction + * @returns true if all expected recipients are found in actual recipients + */ + private recipientsMatch(expected: TransactionRecipient[], actual: TransactionRecipient[]): boolean { + return expected.every((expectedRecipient) => + actual.some((actualRecipient) => _.isEqual(expectedRecipient, actualRecipient)) + ); + } + + /** + * Creates an empty parsed transaction result. + * Used when transaction has no outputs. + */ + private createEmptyParsedTransaction(): ParsedTransaction { + return { + inputs: [], + outputs: [], + fee: new BigNumber(0), + }; + } + + /** + * Calculates the transaction fee from the explanation. + * @param explanation - The transaction explanation + * @returns The fee as a BigNumber + */ + private calculateTransactionFee(explanation: TransactionExplanation): BigNumber { + if (explanation.fee.fee === '') { + return new BigNumber(0); + } + return new BigNumber(explanation.fee.fee); + } + + /** + * Builds the inputs array for a parsed transaction. + * Includes sender input and optionally sponsor input if present. + * + * @param explanation - The transaction explanation + * @param fee - The calculated transaction fee + * @returns Array of transaction inputs + */ + private buildTransactionInputs( + explanation: TransactionExplanation, + fee: BigNumber + ): Array<{ + address: string; + amount: string; + }> { + const senderAddress = explanation.outputs[0].address; + const outputAmount = new BigNumber(explanation.outputAmount); + + // If there's a sponsor, sender only pays for outputs + // Otherwise, sender pays for outputs + fee + const senderAmount = explanation.sponsor ? outputAmount.toFixed() : outputAmount.plus(fee).toFixed(); + + const inputs = [ + { + address: senderAddress, + amount: senderAmount, + }, + ]; + + // Add sponsor input if present + if (explanation.sponsor) { + inputs.push({ + address: explanation.sponsor, + amount: fee.toFixed(), + }); + } + + return inputs; + } + + /** + * Builds the outputs array for a parsed transaction. + * @param explanation - The transaction explanation + * @returns Array of transaction outputs + */ + private buildTransactionOutputs(explanation: TransactionExplanation): Array<{ + address: string; + amount: string; + }> { + return explanation.outputs.map((output) => ({ + address: output.address, + amount: new BigNumber(output.amount).toFixed(), + })); + } + + /** + * Creates a transaction builder factory instance. + * @returns TransactionBuilderFactory for this coin + */ private getTxBuilderFactory(): TransactionBuilderFactory { return new TransactionBuilderFactory(coins.get(this.getChain())); } + /** + * Rebuilds a transaction from its hex representation. + * @param txHex - The transaction hex to rebuild + * @returns The rebuilt transaction + * @throws Error if transaction cannot be rebuilt + */ private async rebuildTransaction(txHex: string): Promise { const txBuilderFactory = this.getTxBuilderFactory(); try { const txBuilder = txBuilderFactory.from(txHex); return (await txBuilder.build()) as Transaction; } catch (err) { - throw new Error(`Failed to rebuild transaction ${err.toString()}`); + throw new Error(`Failed to rebuild transaction: ${err.toString()}`); } } } diff --git a/modules/sdk-coin-iota/src/lib/constants.ts b/modules/sdk-coin-iota/src/lib/constants.ts index 9b5185760c..a5a5f2cd17 100644 --- a/modules/sdk-coin-iota/src/lib/constants.ts +++ b/modules/sdk-coin-iota/src/lib/constants.ts @@ -1,11 +1,90 @@ +// ======================================== +// Address and Digest Length Constants +// ======================================== + +/** + * Length of IOTA addresses in characters (excluding 0x prefix). + * IOTA uses 64-character hexadecimal addresses. + */ export const IOTA_ADDRESS_LENGTH = 64; + +/** + * Length of transaction digest in bytes. + * Used for transaction IDs and hashes. + */ export const IOTA_TRANSACTION_DIGEST_LENGTH = 32; + +/** + * Length of block digest in bytes. + * Used for block IDs and references. + */ export const IOTA_BLOCK_DIGEST_LENGTH = 32; + +// ======================================== +// Cryptographic Constants +// ======================================== + +/** + * Length of Ed25519 signatures in bytes. + * IOTA uses Ed25519 for transaction signing. + */ export const IOTA_SIGNATURE_LENGTH = 64; -export const IOTA_KEY_BYTES_LENGTH = 32; // Ed25519 public key is 32 bytes + +/** + * Length of Ed25519 public keys in bytes. + * Standard Ed25519 key size. + */ +export const IOTA_KEY_BYTES_LENGTH = 32; + +// ======================================== +// Transaction Object Limits +// ======================================== + +/** + * Maximum number of input objects allowed in a single transaction. + * This limit ensures transactions can be processed efficiently by the network. + */ export const MAX_INPUT_OBJECTS = 2048; + +/** + * Maximum number of gas payment objects allowed per transaction. + * When exceeded, objects must be merged before the transaction can be built. + * This prevents transaction size from becoming too large. + */ export const MAX_GAS_PAYMENT_OBJECTS = 256; + +/** + * Maximum number of recipients in a transfer transaction. + * Limits the number of outputs to keep transaction size manageable. + */ +export const MAX_RECIPIENTS = 256; + +// ======================================== +// Gas Configuration Limits +// ======================================== + +/** + * Maximum gas budget allowed for a transaction. + * Used for dry-run simulations to estimate gas costs. + * Set to a very high value (50 billion) to ensure simulation completes. + */ export const MAX_GAS_BUDGET = 50000000000; + +/** + * Maximum gas price for simulated transactions. + * Used when building dry-run transactions for gas estimation. + */ export const MAX_GAS_PRICE = 100000; -export const MAX_RECIPIENTS = 256; // Maximum number of recipients in a transfer transaction + +// ======================================== +// Transaction Command Types +// ======================================== + +/** + * Valid command types for transfer transactions. + * Transfer transactions can only contain these three command types: + * - SplitCoins: Split a coin into multiple coins with specified amounts + * - MergeCoins: Merge multiple coins into a single coin + * - TransferObjects: Transfer coins/objects to recipients + */ export const TRANSFER_TRANSACTION_COMMANDS = ['SplitCoins', 'MergeCoins', 'TransferObjects']; diff --git a/modules/sdk-coin-iota/src/lib/iface.ts b/modules/sdk-coin-iota/src/lib/iface.ts index 28fa028369..194a05dea6 100644 --- a/modules/sdk-coin-iota/src/lib/iface.ts +++ b/modules/sdk-coin-iota/src/lib/iface.ts @@ -6,46 +6,199 @@ import { TransactionType, } from '@bitgo/sdk-core'; +/** + * Extended transaction explanation for IOTA transactions. + * Includes IOTA-specific fields like sender and optional sponsor. + * + * @example + * ```typescript + * const explanation: TransactionExplanation = { + * type: TransactionType.Send, + * sender: '0x1234...', + * sponsor: '0x5678...', // Optional gas sponsor + * outputAmount: '1000000', + * outputs: [...], + * fee: { fee: '5000' } + * }; + * ``` + */ export interface TransactionExplanation extends BaseTransactionExplanation { + /** The type of transaction (e.g., Send, Receive) */ type: BitGoTransactionType; + + /** The address initiating the transaction */ sender: string; + + /** + * Optional gas sponsor address. + * When present, this address pays for the transaction's gas fees + * instead of the sender. + */ sponsor?: string; } +/** + * Represents an IOTA object (coin or NFT) used as transaction input. + * Objects in IOTA are versioned and content-addressable. + * + * @example + * ```typescript + * const coinObject: TransactionObjectInput = { + * objectId: '0x1234...', // Unique object identifier + * version: '42', // Object version number + * digest: 'ABC123...' // Content hash + * }; + * ``` + */ export type TransactionObjectInput = { + /** Unique identifier for the object (64-character hex string) */ objectId: string; + + /** Version number of the object (as string) */ version: string; + + /** Base58-encoded digest of the object's content */ digest: string; }; +/** + * Gas configuration for IOTA transactions. + * All fields are optional to support both simulation and real transactions. + * + * @example + * ```typescript + * const gasData: GasData = { + * gasBudget: 5000000, // Maximum gas units to spend + * gasPrice: 1000, // Price per gas unit + * gasPaymentObjects: [coinObject1, coinObject2] // Coins to pay gas + * }; + * ``` + */ export type GasData = { + /** + * Maximum amount of gas units this transaction can consume. + * Measured in gas units. + */ gasBudget?: number; + + /** + * Price per gas unit in MIST (smallest IOTA unit). + * Total fee = gasBudget * gasPrice + */ gasPrice?: number; + + /** + * Array of coin objects used to pay for gas. + * These objects will be consumed to cover the transaction fee. + */ gasPaymentObjects?: TransactionObjectInput[]; }; /** - * The transaction data returned from the toJson() function of a transaction + * Base transaction data returned from the toJson() function. + * Contains common fields present in all IOTA transactions. + * + * @example + * ```typescript + * const txData: TxData = { + * type: TransactionType.Send, + * sender: '0x1234...', + * gasBudget: 5000000, + * gasPrice: 1000, + * gasPaymentObjects: [...], + * gasSponsor: '0x5678...' // Optional + * }; + * ``` */ export interface TxData { + /** Transaction ID (digest), available after transaction is built */ id?: string; + + /** Address of the transaction sender */ sender: string; + + /** Maximum gas units allocated for this transaction */ gasBudget?: number; + + /** Price per gas unit in MIST */ gasPrice?: number; + + /** Coin objects used to pay for gas */ gasPaymentObjects?: TransactionObjectInput[]; + + /** + * Optional address that sponsors (pays for) the gas. + * If not provided, the sender pays for gas. + */ gasSponsor?: string; + + /** Type of the transaction */ type: TransactionType; } +/** + * Transfer transaction data with recipient information. + * Extends TxData with transfer-specific fields. + * + * @example + * ```typescript + * const transferData: TransferTxData = { + * type: TransactionType.Send, + * sender: '0x1234...', + * recipients: [ + * { address: '0xabcd...', amount: '1000000' }, + * { address: '0xef01...', amount: '2000000' } + * ], + * paymentObjects: [coinObject], + * gasBudget: 5000000, + * gasPrice: 1000, + * gasPaymentObjects: [gasObject] + * }; + * ``` + */ export interface TransferTxData extends TxData { + /** + * Array of recipients and the amounts they receive. + * Each recipient must have a valid IOTA address and amount. + */ recipients: TransactionRecipient[]; + + /** + * Optional coin objects used as payment sources. + * These are split and transferred to recipients. + * If not provided, gas objects are used for payment. + */ paymentObjects?: TransactionObjectInput[]; } +/** + * Options for explaining an IOTA transaction. + * + * @example + * ```typescript + * const explanation = await iota.explainTransaction({ + * txHex: '0x1234...' // Raw transaction hex + * }); + * ``` + */ export interface ExplainTransactionOptions { + /** Raw transaction data in hexadecimal format */ txHex: string; } +/** + * Options for parsing an IOTA transaction. + * Extends base parsing options with IOTA-specific requirements. + * + * @example + * ```typescript + * const parsed = await iota.parseTransaction({ + * txHex: '0x1234...' // Raw transaction hex + * }); + * // Returns: { inputs: [...], outputs: [...], fee: BigNumber } + * ``` + */ export interface IotaParseTransactionOptions extends BaseParseTransactionOptions { + /** Raw transaction data in hexadecimal format */ txHex: string; } diff --git a/modules/sdk-coin-iota/src/lib/keyPair.ts b/modules/sdk-coin-iota/src/lib/keyPair.ts index 7466c79f73..b1e92ebb44 100644 --- a/modules/sdk-coin-iota/src/lib/keyPair.ts +++ b/modules/sdk-coin-iota/src/lib/keyPair.ts @@ -1,40 +1,135 @@ import { DefaultKeys, Ed25519KeyPair, KeyPairOptions } from '@bitgo/sdk-core'; import utils from './utils'; +/** + * IOTA KeyPair implementation using Ed25519 cryptography. + * + * This class extends the Ed25519KeyPair base class and provides IOTA-specific + * key pair functionality. It's primarily used for TSS (Threshold Signature Scheme) + * operations where private keys are managed through multi-party computation. + * + * @example + * ```typescript + * // Generate a random key pair + * const keyPair = new KeyPair(); + * + * // Generate from a seed + * const keyPair = new KeyPair({ seed: Buffer.from('...') }); + * + * // Generate from a public key + * const keyPair = new KeyPair({ pub: '8c26e54e36c902c5...' }); + * + * // Get the IOTA address + * const address = keyPair.getAddress(); + * ``` + */ export class KeyPair extends Ed25519KeyPair { /** - * Public constructor. By default, creates a key pair with a random master seed. + * Creates a new IOTA key pair. * - * @param { KeyPairOptions } source Either a master seed, a private key, or a public key + * @param source - Optional configuration for key pair generation: + * - seed: Buffer - Generate deterministic key pair from seed + * - prv: string - Import from private key (not used for TSS) + * - pub: string - Import from public key only + * - If omitted, generates a random key pair + * + * @example + * ```typescript + * // Random key pair + * const randomKeyPair = new KeyPair(); + * + * // From seed (deterministic) + * const deterministicKeyPair = new KeyPair({ + * seed: Buffer.from('my-seed-phrase') + * }); + * + * // From public key only (for verification) + * const verificationKeyPair = new KeyPair({ + * pub: '8c26e54e36c902c5452e8b44e28abc5aaa6c3faaf12b4c0e8a38b4c9da0c0a6a' + * }); + * ``` */ constructor(source?: KeyPairOptions) { super(source); } - /** @inheritdoc */ + /** + * Returns the key pair as a DefaultKeys object. + * Always includes the public key, and includes the private key if available. + * + * @returns Object containing pub (always) and prv (if available) + * + * @example + * ```typescript + * const keyPair = new KeyPair(); + * const keys = keyPair.getKeys(); + * console.log(keys.pub); // '8c26e54e...' + * console.log(keys.prv); // '1a2b3c...' or undefined + * ``` + */ getKeys(): DefaultKeys { const result: DefaultKeys = { pub: this.keyPair.pub }; + if (this.keyPair.prv) { result.prv = this.keyPair.prv; } + return result; } - /** @inheritdoc */ + /** + * Records keys from a private key in protocol format. + * + * **Not implemented for IOTA.** + * + * IOTA uses TSS (Threshold Signature Scheme) where private keys are never + * reconstructed in full. Instead, key shares are distributed across multiple + * parties and signing is performed through multi-party computation. + * + * @param prv - The private key (unused) + * @throws Error always - method not supported for TSS-based signing + */ recordKeysFromPrivateKeyInProtocolFormat(prv: string): DefaultKeys { - // We don't use private keys for IOTA since it's implemented for TSS. - throw new Error('Method not implemented.'); + throw new Error('Method not implemented. IOTA uses TSS and does not reconstruct private keys.'); } - /** @inheritdoc */ + /** + * Records keys from a public key in protocol format. + * Validates the public key and returns it in the DefaultKeys format. + * + * @param pub - The Ed25519 public key (hex string) + * @returns DefaultKeys object containing only the public key + * @throws Error if the public key is invalid + * + * @example + * ```typescript + * const keys = keyPair.recordKeysFromPublicKeyInProtocolFormat( + * '8c26e54e36c902c5452e8b44e28abc5aaa6c3faaf12b4c0e8a38b4c9da0c0a6a' + * ); + * console.log(keys.pub); // '8c26e54e...' + * console.log(keys.prv); // undefined + * ``` + */ recordKeysFromPublicKeyInProtocolFormat(pub: string): DefaultKeys { if (!utils.isValidPublicKey(pub)) { - throw new Error(`Invalid Public Key ${pub}`); + throw new Error(`Invalid Public Key: ${pub}`); } return { pub }; } - /** @inheritdoc */ + /** + * Derives the IOTA address from this key pair's public key. + * Uses the IOTA-specific address derivation algorithm. + * + * @returns The IOTA address (64-character hex string with 0x prefix) + * + * @example + * ```typescript + * const keyPair = new KeyPair(); + * const address = keyPair.getAddress(); + * console.log(address); // '0x9882188ba3e8070a9bb06ae9446cf607914ee8ee...' + * ``` + */ getAddress(): string { return utils.getAddressFromPublicKey(this.keyPair.pub); } diff --git a/modules/sdk-coin-iota/src/lib/utils.ts b/modules/sdk-coin-iota/src/lib/utils.ts index e89533c4d5..8dbf9b30b4 100644 --- a/modules/sdk-coin-iota/src/lib/utils.ts +++ b/modules/sdk-coin-iota/src/lib/utils.ts @@ -9,69 +9,185 @@ import { Ed25519PublicKey } from '@iota/iota-sdk/keypairs/ed25519'; import { Transaction as IotaTransaction } from '@iota/iota-sdk/transactions'; import { toBase64, fromBase64 } from '@iota/iota-sdk/utils'; +/** + * Utility class for IOTA-specific validation and conversion operations. + * Implements the BaseUtils interface and provides methods for validating + * addresses, keys, signatures, and transaction data. + */ export class Utils implements BaseUtils { - /** @inheritdoc */ + // ======================================== + // Address and ID Validation + // ======================================== + + /** + * Validates an IOTA address format. + * IOTA addresses are 64-character hex strings prefixed with '0x'. + * + * @param address - The address to validate + * @returns true if the address is valid + * + * @example + * ```typescript + * utils.isValidAddress('0x1234...') // true + * utils.isValidAddress('invalid') // false + * ``` + */ isValidAddress(address: string): boolean { return this.isValidHex(address, IOTA_ADDRESS_LENGTH); } - /** @inheritdoc */ + /** + * Validates an IOTA block ID (digest). + * Block IDs are 32-byte base58-encoded strings. + * + * @param hash - The block ID to validate + * @returns true if the block ID is valid + */ isValidBlockId(hash: string): boolean { return isBase58(hash, IOTA_BLOCK_DIGEST_LENGTH); } - /** @inheritdoc */ + /** + * Validates an IOTA transaction ID (digest). + * Transaction IDs are 32-byte base58-encoded strings. + * + * @param txId - The transaction ID to validate + * @returns true if the transaction ID is valid + */ + isValidTransactionId(txId: string): boolean { + return isBase58(txId, IOTA_TRANSACTION_DIGEST_LENGTH); + } + + // ======================================== + // Key Validation + // ======================================== + + /** + * Validates an Ed25519 private key format. + * + * @param key - The private key to validate (hex string) + * @returns true if the private key is valid + */ isValidPrivateKey(key: string): boolean { return isValidEd25519SecretKey(key); } - /** @inheritdoc */ + /** + * Validates an Ed25519 public key format. + * + * @param key - The public key to validate (hex string) + * @returns true if the public key is valid + */ isValidPublicKey(key: string): boolean { return isValidEd25519PublicKey(key); } - /** @inheritdoc */ + // ======================================== + // Signature and Transaction Validation + // ======================================== + + /** + * Validates an IOTA signature format. + * Signatures must be base64-encoded and exactly 64 bytes when decoded. + * + * @param signature - The base64-encoded signature to validate + * @returns true if the signature is valid + */ isValidSignature(signature: string): boolean { try { - return fromBase64(signature).length === IOTA_SIGNATURE_LENGTH; - } catch (e) { + const decodedSignature = fromBase64(signature); + return decodedSignature.length === IOTA_SIGNATURE_LENGTH; + } catch (error) { + // Invalid base64 or decoding error return false; } } - /** @inheritdoc */ - isValidTransactionId(txId: string): boolean { - return isBase58(txId, IOTA_TRANSACTION_DIGEST_LENGTH); + /** + * Validates a raw IOTA transaction format. + * Attempts to parse the transaction using the IOTA SDK. + * + * @param rawTransaction - The raw transaction (base64 string or Uint8Array) + * @returns true if the transaction can be parsed + */ + isValidRawTransaction(rawTransaction: string | Uint8Array): boolean { + try { + IotaTransaction.from(rawTransaction); + return true; + } catch (error) { + return false; + } } + // ======================================== + // Conversion and Utility Methods + // ======================================== + + /** + * Validates a hex string with a specific length requirement. + * Checks for '0x' or '0X' prefix followed by the specified number of hex characters. + * + * @param value - The hex string to validate + * @param length - The required length (number of hex characters, excluding prefix) + * @returns true if the hex string matches the format and length + */ isValidHex(value: string, length: number): boolean { const regex = new RegExp(`^(0x|0X)[a-fA-F0-9]{${length}}$`); return regex.test(value); } + /** + * Converts a value to a base64-encoded string. + * Handles both Uint8Array and hex string inputs. + * + * @param value - The value to encode (Uint8Array or hex string) + * @returns Base64-encoded string + * + * @example + * ```typescript + * utils.getBase64String(new Uint8Array([1, 2, 3])) + * utils.getBase64String('0x010203') + * ``` + */ getBase64String(value: string | Uint8Array): string { if (value instanceof Uint8Array) { return toBase64(value); - } else { - return toBase64(Buffer.from(value, 'hex')); } + return toBase64(Buffer.from(value, 'hex')); } + /** + * Derives an IOTA address from an Ed25519 public key. + * Uses the IOTA SDK's address derivation algorithm. + * + * @param publicKey - The Ed25519 public key (hex string) + * @returns The derived IOTA address + * + * @example + * ```typescript + * const address = utils.getAddressFromPublicKey('8c26e54e36c902c5...') + * // Returns: '0x9882188ba3e8070a...' + * ``` + */ getAddressFromPublicKey(publicKey: string): string { const iotaPublicKey = new Ed25519PublicKey(Buffer.from(publicKey, 'hex')); return iotaPublicKey.toIotaAddress(); } - - isValidRawTransaction(rawTransaction: string | Uint8Array): boolean { - try { - IotaTransaction.from(rawTransaction); - } catch (e) { - return false; - } - return true; - } } +/** + * Singleton instance of the Utils class. + * Use this for all IOTA utility operations throughout the SDK. + * + * @example + * ```typescript + * import utils from './utils'; + * + * if (utils.isValidAddress(address)) { + * // Process valid address + * } + * ``` + */ const utils = new Utils(); export default utils; diff --git a/modules/sdk-coin-iota/test/unit/iota.ts b/modules/sdk-coin-iota/test/unit/iota.ts index 1b3eccd920..f9be78107c 100644 --- a/modules/sdk-coin-iota/test/unit/iota.ts +++ b/modules/sdk-coin-iota/test/unit/iota.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import { coins, GasTankAccountCoin } from '@bitgo/statics'; import * as testData from '../resources/iota'; import { TransactionType } from '@bitgo/sdk-core'; +import { createTransferBuilderWithGas } from './helpers/testHelpers'; describe('IOTA:', function () { let bitgo: TestBitGoAPI; @@ -86,19 +87,20 @@ describe('IOTA:', function () { ]; }); - it('should return true when validating a well formatted address prefixed with 0x', async function () { - const address = '0xf941ae3cbe5645dccc15da8346b533f7f91f202089a5521653c062b2ff10b304'; - basecoin.isValidAddress(address).should.equal(true); - }); - - it('should return false when validating an old address', async function () { - const address = '0x2959bfc3fdb7dc23fed8deba2fafb70f3e606a59'; - basecoin.isValidAddress(address).should.equal(false); - }); + const addressValidationTests = [ + { + address: '0xf941ae3cbe5645dccc15da8346b533f7f91f202089a5521653c062b2ff10b304', + valid: true, + description: 'valid 0x-prefixed address', + }, + { address: '0x2959bfc3fdb7dc23fed8deba2fafb70f3e606a59', valid: false, description: 'old format address' }, + { address: 'wrongaddress', valid: false, description: 'incorrectly formatted address' }, + ]; - it('should return false when validating an incorrectly formatted', async function () { - const address = 'wrongaddress'; - basecoin.isValidAddress(address).should.equal(false); + addressValidationTests.forEach(({ address, valid, description }) => { + it(`should return ${valid} when validating ${description}`, function () { + basecoin.isValidAddress(address).should.equal(valid); + }); }); it('should return true for isWalletAddress with valid address for index 4', async function () { @@ -151,12 +153,7 @@ describe('IOTA:', function () { }); it('should call explainTransaction on transaction object', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const explanation = tx.explainTransaction(); @@ -182,19 +179,14 @@ describe('IOTA:', function () { ); }); - it('should verify transaction with matching recipients using JSON', async function () { - // Build transaction and get JSON instead of broadcast format to avoid parsing issues - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + it('should verify transaction with matching recipients', async function () { + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const txHex = Buffer.from(await tx.toBroadcastFormat(), 'base64').toString('hex'); + should.equal( await basecoin.verifyTransaction({ - txPrebuild: { txHex: txHex }, + txPrebuild: { txHex }, txParams: { recipients: testData.recipients }, }), true @@ -202,18 +194,10 @@ describe('IOTA:', function () { }); it('should verify transaction with recipients containing extra fields', async function () { - // The verification should only compare address, amount, and tokenName - // Extra fields should be ignored - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const txHex = Buffer.from(await tx.toBroadcastFormat(), 'base64').toString('hex'); - // Add extra fields to recipients that should be ignored during comparison const recipientsWithExtraFields = testData.recipients.map((r) => ({ ...r, extraField: 'should be ignored', @@ -222,7 +206,7 @@ describe('IOTA:', function () { should.equal( await basecoin.verifyTransaction({ - txPrebuild: { txHex: txHex }, + txPrebuild: { txHex }, txParams: { recipients: recipientsWithExtraFields }, }), true @@ -230,27 +214,21 @@ describe('IOTA:', function () { }); it('should detect mismatched recipients', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const txHex = Buffer.from(await tx.toBroadcastFormat(), 'base64').toString('hex'); - // Create mismatched recipients with different addresses/amounts const mismatchedRecipients = [ { - address: testData.addresses.validAddresses[2], // Different address - amount: '9999', // Different amount + address: testData.addresses.validAddresses[2], + amount: '9999', }, ]; await assert.rejects( async () => await basecoin.verifyTransaction({ - txPrebuild: { txHex: txHex }, + txPrebuild: { txHex }, txParams: { recipients: mismatchedRecipients }, }), /Tx recipients does not match with expected txParams recipients/ @@ -258,18 +236,13 @@ describe('IOTA:', function () { }); it('should verify transaction without recipients parameter', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const txHex = Buffer.from(await tx.toBroadcastFormat(), 'base64').toString('hex'); should.equal( await basecoin.verifyTransaction({ - txPrebuild: { txHex: txHex }, + txPrebuild: { txHex }, txParams: {}, }), true @@ -286,36 +259,23 @@ describe('IOTA:', function () { }); it('should parse transaction using JSON format', async function () { - // Build a transaction - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; - - // Get the transaction explanation (which is what parseTransaction returns internally) const explanation = tx.explainTransaction(); - // Verify the parsed data structure explanation.should.have.property('id'); explanation.should.have.property('outputs'); explanation.should.have.property('outputAmount'); explanation.should.have.property('fee'); - // Verify outputs match recipients explanation.outputs.length.should.equal(testData.recipients.length); explanation.outputs.forEach((output, index) => { output.address.should.equal(testData.recipients[index].address); output.amount.should.equal(testData.recipients[index].amount); }); - // Verify output amount const totalAmount = testData.recipients.reduce((sum, r) => sum + Number(r.amount), 0); explanation.outputAmount.should.equal(totalAmount.toString()); - - // Verify fee explanation.fee.fee.should.equal(testData.GAS_BUDGET.toString()); }); @@ -360,17 +320,12 @@ describe('IOTA:', function () { describe('getSignablePayload', () => { it('should get signable payload from transaction directly', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const signablePayload = tx.signablePayload; signablePayload.should.be.instanceOf(Buffer); - signablePayload.length.should.equal(32); // Blake2b hash is 32 bytes + signablePayload.length.should.equal(32); }); it('should throw error for invalid transaction', async function () { @@ -381,54 +336,28 @@ describe('IOTA:', function () { }); it('should generate consistent signable payload for identical transactions', async function () { - // Build first transaction - const txBuilder1 = factory.getTransferBuilder(); - txBuilder1.sender(testData.sender.address); - txBuilder1.recipients(testData.recipients); - txBuilder1.paymentObjects(testData.paymentObjects); - txBuilder1.gasData(testData.gasData); - + const txBuilder1 = createTransferBuilderWithGas(); const tx1 = (await txBuilder1.build()) as TransferTransaction; - const payload1 = tx1.signablePayload; - - // Build second identical transaction - const txBuilder2 = factory.getTransferBuilder(); - txBuilder2.sender(testData.sender.address); - txBuilder2.recipients(testData.recipients); - txBuilder2.paymentObjects(testData.paymentObjects); - txBuilder2.gasData(testData.gasData); + const txBuilder2 = createTransferBuilderWithGas(); const tx2 = (await txBuilder2.build()) as TransferTransaction; - const payload2 = tx2.signablePayload; - // Payloads should be identical for identical transactions - payload1.toString('hex').should.equal(payload2.toString('hex')); + tx1.signablePayload.toString('hex').should.equal(tx2.signablePayload.toString('hex')); }); it('should generate different signable payloads for different transactions', async function () { - // Build first transaction - const txBuilder1 = factory.getTransferBuilder(); - txBuilder1.sender(testData.sender.address); - txBuilder1.recipients(testData.recipients); - txBuilder1.paymentObjects(testData.paymentObjects); - txBuilder1.gasData(testData.gasData); - + const txBuilder1 = createTransferBuilderWithGas(); const tx1 = (await txBuilder1.build()) as TransferTransaction; - const payload1 = tx1.signablePayload; - // Build second transaction with different recipient const differentRecipients = [{ address: testData.addresses.validAddresses[0], amount: '5000' }]; const txBuilder2 = factory.getTransferBuilder(); txBuilder2.sender(testData.sender.address); txBuilder2.recipients(differentRecipients); txBuilder2.paymentObjects(testData.paymentObjects); txBuilder2.gasData(testData.gasData); - const tx2 = (await txBuilder2.build()) as TransferTransaction; - const payload2 = tx2.signablePayload; - // Payloads should be different for different transactions - payload1.toString('hex').should.not.equal(payload2.toString('hex')); + tx1.signablePayload.toString('hex').should.not.equal(tx2.signablePayload.toString('hex')); }); it('should throw error when getting payload from simulate transaction', async function () { @@ -436,11 +365,9 @@ describe('IOTA:', function () { txBuilder.sender(testData.sender.address); txBuilder.recipients(testData.recipients); txBuilder.paymentObjects(testData.paymentObjects); - // Don't set gasData - this will be a simulate transaction const tx = (await txBuilder.build()) as TransferTransaction; should.equal(tx.isSimulateTx, true); - should(() => tx.signablePayload).throwError('Cannot sign a simulate tx'); }); }); @@ -484,13 +411,8 @@ describe('IOTA:', function () { describe('Transaction with Gas Sponsor', () => { it('should build transaction with gas sponsor', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); + const txBuilder = createTransferBuilderWithGas(); txBuilder.gasSponsor(testData.gasSponsor.address); - const tx = (await txBuilder.build()) as TransferTransaction; should.equal(tx.sender, testData.sender.address); @@ -500,13 +422,8 @@ describe('IOTA:', function () { }); it('should handle transaction where sender and gas sponsor are same', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - txBuilder.gasSponsor(testData.sender.address); // Same as sender - + const txBuilder = createTransferBuilderWithGas(); + txBuilder.gasSponsor(testData.sender.address); const tx = (await txBuilder.build()) as TransferTransaction; should.equal(tx.sender, testData.sender.address); @@ -516,32 +433,17 @@ describe('IOTA:', function () { describe('Transaction ID Consistency', () => { it('should generate same ID for identical transactions', async function () { - const txBuilder1 = factory.getTransferBuilder(); - txBuilder1.sender(testData.sender.address); - txBuilder1.recipients(testData.recipients); - txBuilder1.paymentObjects(testData.paymentObjects); - txBuilder1.gasData(testData.gasData); - + const txBuilder1 = createTransferBuilderWithGas(); const tx1 = (await txBuilder1.build()) as TransferTransaction; - const txBuilder2 = factory.getTransferBuilder(); - txBuilder2.sender(testData.sender.address); - txBuilder2.recipients(testData.recipients); - txBuilder2.paymentObjects(testData.paymentObjects); - txBuilder2.gasData(testData.gasData); - + const txBuilder2 = createTransferBuilderWithGas(); const tx2 = (await txBuilder2.build()) as TransferTransaction; should.equal(tx1.id, tx2.id); }); it('should generate different IDs for different transactions', async function () { - const txBuilder1 = factory.getTransferBuilder(); - txBuilder1.sender(testData.sender.address); - txBuilder1.recipients(testData.recipients); - txBuilder1.paymentObjects(testData.paymentObjects); - txBuilder1.gasData(testData.gasData); - + const txBuilder1 = createTransferBuilderWithGas(); const tx1 = (await txBuilder1.build()) as TransferTransaction; const differentRecipients = [{ address: testData.addresses.validAddresses[0], amount: '9999' }]; @@ -550,7 +452,6 @@ describe('IOTA:', function () { txBuilder2.recipients(differentRecipients); txBuilder2.paymentObjects(testData.paymentObjects); txBuilder2.gasData(testData.gasData); - const tx2 = (await txBuilder2.build()) as TransferTransaction; should.notEqual(tx1.id, tx2.id); @@ -598,12 +499,7 @@ describe('IOTA:', function () { }); it('should return gas fee from transaction', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const fee = tx.getFee(); @@ -656,12 +552,7 @@ describe('IOTA:', function () { }); it('should handle transaction type correctly', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; should.equal(tx.type, TransactionType.Send); @@ -670,21 +561,11 @@ describe('IOTA:', function () { describe('Transaction Serialization Formats', () => { it('should serialize to consistent broadcast format', async function () { - const txBuilder1 = factory.getTransferBuilder(); - txBuilder1.sender(testData.sender.address); - txBuilder1.recipients(testData.recipients); - txBuilder1.paymentObjects(testData.paymentObjects); - txBuilder1.gasData(testData.gasData); - + const txBuilder1 = createTransferBuilderWithGas(); const tx1 = (await txBuilder1.build()) as TransferTransaction; const broadcast1 = await tx1.toBroadcastFormat(); - const txBuilder2 = factory.getTransferBuilder(); - txBuilder2.sender(testData.sender.address); - txBuilder2.recipients(testData.recipients); - txBuilder2.paymentObjects(testData.paymentObjects); - txBuilder2.gasData(testData.gasData); - + const txBuilder2 = createTransferBuilderWithGas(); const tx2 = (await txBuilder2.build()) as TransferTransaction; const broadcast2 = await tx2.toBroadcastFormat(); @@ -692,32 +573,20 @@ describe('IOTA:', function () { }); it('should produce valid base64 broadcast format', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const broadcast = await tx.toBroadcastFormat(); - // Check if it's valid base64 should.equal(typeof broadcast, 'string'); should.equal(/^[A-Za-z0-9+/]*={0,2}$/.test(broadcast), true); should.equal(broadcast.length > 0, true); - // Should be able to decode const decoded = Buffer.from(broadcast, 'base64'); should.equal(decoded.length > 0, true); }); it('should maintain JSON serialization consistency', async function () { - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + const txBuilder = createTransferBuilderWithGas(); const tx = (await txBuilder.build()) as TransferTransaction; const json1 = tx.toJson(); const json2 = tx.toJson(); diff --git a/modules/sdk-coin-iota/test/unit/keyPair.ts b/modules/sdk-coin-iota/test/unit/keyPair.ts index 6d2cd779f2..a0ba1226e4 100644 --- a/modules/sdk-coin-iota/test/unit/keyPair.ts +++ b/modules/sdk-coin-iota/test/unit/keyPair.ts @@ -32,80 +32,55 @@ describe('Iota KeyPair', function () { basecoin = bitgo.coin('tiota'); }); - describe('should create a valid KeyPair', () => { - it('from an empty value', async () => { + describe('KeyPair Creation', () => { + it('should create keypair with public and private keys from empty value', async () => { const keyPair = new KeyPair(); - should.exists(keyPair.getKeys().prv); - should.exists(keyPair.getKeys().pub); - const address = await utils.getAddressFromPublicKey(keyPair.getKeys().pub); - console.log('address:', address); - should.exists(address); + const keys = keyPair.getKeys(); + + should.exist(keys.prv); + should.exist(keys.pub); + + const address = await utils.getAddressFromPublicKey(keys.pub); + should.exist(address); }); - }); - describe('Keypair from derived Public Key', () => { - it('should create keypair with just derived public key', () => { + it('should create keypair from derived public key', () => { const keyPair = new KeyPair({ pub: rootPublicKey }); keyPair.getKeys().pub.should.equal(rootPublicKey); - }); - - it('should derived ed25519 public key should be valid', () => { utils.isValidPublicKey(rootPublicKey).should.be.true(); }); - }); - describe('Keypair from random seed', () => { - it('should generate a keypair from random seed', function () { + it('should create keypair from random seed', () => { const keyPair = basecoin.generateKeyPair(); keyPair.should.have.property('pub'); keyPair.should.have.property('prv'); basecoin.isValidPub(keyPair.pub).should.equal(true); }); - }); - - describe('should fail to create a KeyPair', function () { - it('from an invalid public key', () => { - const source = { - pub: '01D63D', - }; - - assert.throws(() => new KeyPair(source)); - }); - - it('from an invalid public key with 0x prefix', () => { - const source = { - pub: '0x' + 'a'.repeat(64), - }; - - assert.throws(() => new KeyPair(source)); - }); - - it('from a too short public key', () => { - const source = { - pub: 'abc123', - }; - - assert.throws(() => new KeyPair(source)); - }); - it('from a too long public key', () => { - const source = { - pub: 'a'.repeat(128), - }; - - assert.throws(() => new KeyPair(source)); - }); - }); - - describe('KeyPair operations', () => { - it('should get keys from keypair', () => { + it('should create keypair without private key', () => { const keyPair = new KeyPair({ pub: rootPublicKey }); const keys = keyPair.getKeys(); keys.should.have.property('pub'); keys.pub.should.equal(rootPublicKey); + should.not.exist(keys.prv); + }); + + const invalidPublicKeys = [ + { pub: '01D63D', description: 'invalid format' }, + { pub: '0x' + 'a'.repeat(64), description: '0x prefix' }, + { pub: 'abc123', description: 'too short' }, + { pub: 'a'.repeat(128), description: 'too long' }, + ]; + + invalidPublicKeys.forEach(({ pub, description }) => { + it(`should reject public key with ${description}`, () => { + assert.throws(() => new KeyPair({ pub })); + }); }); + }); + describe('KeyPair Operations', () => { it('should generate random keypair each time', () => { const keyPair1 = basecoin.generateKeyPair(); const keyPair2 = basecoin.generateKeyPair(); @@ -114,15 +89,6 @@ describe('Iota KeyPair', function () { keyPair1.prv.should.not.equal(keyPair2.prv); }); - it('should create keypair without private key', () => { - const keyPair = new KeyPair({ pub: rootPublicKey }); - const keys = keyPair.getKeys(); - - keys.should.have.property('pub'); - keys.pub.should.equal(rootPublicKey); - should.not.exist(keys.prv); - }); - it('should validate generated public keys', () => { const keyPair = basecoin.generateKeyPair(); basecoin.isValidPub(keyPair.pub).should.equal(true); @@ -130,28 +96,28 @@ describe('Iota KeyPair', function () { }); }); - describe('Address derivation', () => { - it('should derive address from public key', async () => { + describe('Address Derivation', () => { + it('should derive valid address from public key', () => { const keyPair = new KeyPair({ pub: rootPublicKey }); - const address = await utils.getAddressFromPublicKey(keyPair.getKeys().pub); + const address = keyPair.getAddress(); should.exist(address); utils.isValidAddress(address).should.be.true(); }); - it('should derive same address from same public key', async () => { - const address1 = await utils.getAddressFromPublicKey(rootPublicKey); - const address2 = await utils.getAddressFromPublicKey(rootPublicKey); + it('should derive same address from same public key', () => { + const address1 = utils.getAddressFromPublicKey(rootPublicKey); + const address2 = utils.getAddressFromPublicKey(rootPublicKey); address1.should.equal(address2); }); - it('should derive different addresses from different public keys', async () => { + it('should derive different addresses from different public keys', () => { const keyPair1 = basecoin.generateKeyPair(); const keyPair2 = basecoin.generateKeyPair(); - const address1 = await utils.getAddressFromPublicKey(keyPair1.pub); - const address2 = await utils.getAddressFromPublicKey(keyPair2.pub); + const address1 = utils.getAddressFromPublicKey(keyPair1.pub); + const address2 = utils.getAddressFromPublicKey(keyPair2.pub); address1.should.not.equal(address2); }); diff --git a/modules/sdk-coin-iota/test/unit/utils.ts b/modules/sdk-coin-iota/test/unit/utils.ts index b1382d7165..ac3957f466 100644 --- a/modules/sdk-coin-iota/test/unit/utils.ts +++ b/modules/sdk-coin-iota/test/unit/utils.ts @@ -1,294 +1,245 @@ import * as testData from '../resources/iota'; import should from 'should'; import utils from '../../src/lib/utils'; -import { TransactionBuilderFactory } from '../../src'; -import { coins } from '@bitgo/statics'; +import { createTransferBuilderWithGas } from './helpers/testHelpers'; describe('Iota util library', function () { - describe('isValidAddress', function () { - it('should succeed to validate addresses', function () { - for (const address of testData.addresses.validAddresses) { - should.equal(utils.isValidAddress(address), true); - } - }); - - it('should fail to validate invalid addresses', function () { - for (const address of testData.addresses.invalidAddresses) { - should.doesNotThrow(() => utils.isValidAddress(address)); - should.equal(utils.isValidAddress(address), false); - } - // @ts-expect-error Testing for missing param, should not throw an error - should.doesNotThrow(() => utils.isValidAddress(undefined)); - // @ts-expect-error Testing for missing param, should return false - should.equal(utils.isValidAddress(undefined), false); - }); - - it('should validate addresses with correct length', function () { - // IOTA addresses are 64 characters hex with 0x prefix - const validAddress = '0x' + 'a'.repeat(64); - should.equal(utils.isValidAddress(validAddress), true); - }); - - it('should reject addresses with incorrect length', function () { - const shortAddress = '0x' + 'a'.repeat(32); - const longAddress = '0x' + 'a'.repeat(128); - should.equal(utils.isValidAddress(shortAddress), false); - should.equal(utils.isValidAddress(longAddress), false); - }); - - it('should reject addresses without 0x prefix', function () { - const addressWithoutPrefix = 'a'.repeat(64); - should.equal(utils.isValidAddress(addressWithoutPrefix), false); - }); - - it('should reject addresses with non-hex characters', function () { - const invalidHex = '0x' + 'g'.repeat(64); - should.equal(utils.isValidAddress(invalidHex), false); + describe('Address Validation', function () { + it('should validate all correct addresses', function () { + testData.addresses.validAddresses.forEach((address) => { + utils.isValidAddress(address).should.be.true(); + }); + }); + + it('should reject all invalid addresses', function () { + testData.addresses.invalidAddresses.forEach((address) => { + utils.isValidAddress(address).should.be.false(); + }); + }); + + const addressTestCases = [ + { address: '0x' + 'a'.repeat(64), valid: true, description: 'correct length (64 hex chars)' }, + { address: '0x' + 'a'.repeat(32), valid: false, description: 'too short' }, + { address: '0x' + 'a'.repeat(128), valid: false, description: 'too long' }, + { address: 'a'.repeat(64), valid: false, description: 'missing 0x prefix' }, + { address: '0x' + 'g'.repeat(64), valid: false, description: 'non-hex characters' }, + { address: undefined, valid: false, description: 'undefined' }, + ]; + + addressTestCases.forEach(({ address, valid, description }) => { + it(`should ${valid ? 'accept' : 'reject'} address with ${description}`, function () { + // @ts-expect-error Testing for undefined + utils.isValidAddress(address).should.equal(valid); + }); }); }); - describe('isValidPublicKey', function () { - it('should validate correct public keys', function () { - // without 0x prefix (64 hex chars) - should.equal(true, utils.isValidPublicKey('b2051899478edeb36a79d1d16dfec56dc3a6ebd29fbbbb4a4ef2dfaf46043355')); - should.equal(true, utils.isValidPublicKey(testData.sender.publicKey)); - }); + describe('Public Key Validation', function () { + const validPublicKeys = [ + 'b2051899478edeb36a79d1d16dfec56dc3a6ebd29fbbbb4a4ef2dfaf46043355', + testData.sender.publicKey, + ]; - it('should reject public keys with 0x prefix', function () { - should.equal(false, utils.isValidPublicKey('0x413f7fa8beb54459e1e9ede3af3b12e5a4a3550390bb616da30dd72017701263')); + it('should validate all correct public keys', function () { + validPublicKeys.forEach((key) => { + utils.isValidPublicKey(key).should.be.true(); + }); }); - it('should reject invalid public keys', function () { - should.equal(false, utils.isValidPublicKey('invalid')); - should.equal(false, utils.isValidPublicKey('')); - should.equal(false, utils.isValidPublicKey('123')); - }); + const invalidPublicKeyTestCases = [ + { key: '0x413f7fa8beb54459e1e9ede3af3b12e5a4a3550390bb616da30dd72017701263', description: 'with 0x prefix' }, + { key: 'invalid', description: 'invalid format' }, + { key: '', description: 'empty string' }, + { key: '123', description: 'too short' }, + { key: 'a'.repeat(32), description: 'incorrect length (too short)' }, + { key: 'a'.repeat(128), description: 'incorrect length (too long)' }, + ]; - it('should reject public keys with incorrect length', function () { - should.equal(false, utils.isValidPublicKey('a'.repeat(32))); // Too short - should.equal(false, utils.isValidPublicKey('a'.repeat(128))); // Too long + invalidPublicKeyTestCases.forEach(({ key, description }) => { + it(`should reject public key ${description}`, function () { + utils.isValidPublicKey(key).should.be.false(); + }); }); }); - describe('isValidPrivateKey', function () { - it('should validate ed25519 secret keys', function () { - // Ed25519 secret keys (as used by tweetnacl) are 64 bytes (128 hex chars) - // This includes the 32-byte seed + 32-byte public key + describe('Private Key Validation', function () { + it('should validate ed25519 secret keys with correct length', function () { + // Ed25519 secret keys are 128 hex chars (64 bytes: 32-byte seed + 32-byte public key) const validSecretKey = '0'.repeat(128); - should.equal(utils.isValidPrivateKey(validSecretKey), true); + utils.isValidPrivateKey(validSecretKey).should.be.true(); }); - it('should reject invalid private keys', function () { - should.equal(utils.isValidPrivateKey('invalid'), false); - should.equal(utils.isValidPrivateKey(''), false); - should.equal(utils.isValidPrivateKey('123'), false); - }); + const invalidPrivateKeyTestCases = [ + { key: 'invalid', description: 'invalid format' }, + { key: '', description: 'empty string' }, + { key: '123', description: 'too short' }, + { key: 'a'.repeat(32), description: '16 bytes (too short)' }, + { key: 'a'.repeat(64), description: '32 bytes (seed only, not full secret)' }, + { key: 'a'.repeat(256), description: '128 bytes (too long)' }, + ]; - it('should reject private keys with wrong length', function () { - should.equal(utils.isValidPrivateKey('a'.repeat(32)), false); // Too short (16 bytes) - should.equal(utils.isValidPrivateKey('a'.repeat(64)), false); // Seed length, not full secret key (32 bytes) - should.equal(utils.isValidPrivateKey('a'.repeat(256)), false); // Too long (128 bytes) + invalidPrivateKeyTestCases.forEach(({ key, description }) => { + it(`should reject private key: ${description}`, function () { + utils.isValidPrivateKey(key).should.be.false(); + }); }); }); - describe('isValidTransactionId', function () { - it('should validate correct transaction IDs', function () { - should.equal(true, utils.isValidTransactionId('BftEk3BeKUWTj9uzVGntd4Ka16QZG8hUnr6KsAb7q7bt')); - }); - - it('should reject invalid transaction IDs', function () { - should.equal( - false, - utils.isValidTransactionId('0xff86b121181a43d03df52e8930785af3dda944ec87654cdba3a378ff518cd75b') - ); - should.equal(false, utils.isValidTransactionId('BftEk3BeKUWTj9uzVGntd4Ka16QZG8hUnr6KsAb7q7b53t')); // Wrong length - }); - - it('should reject hex strings', function () { - should.equal(false, utils.isValidTransactionId('0xabcdef123456')); - }); - - it('should reject empty strings', function () { - should.equal(false, utils.isValidTransactionId('')); + describe('Transaction and Block ID Validation', function () { + it('should validate correct transaction ID (base58)', function () { + utils.isValidTransactionId('BftEk3BeKUWTj9uzVGntd4Ka16QZG8hUnr6KsAb7q7bt').should.be.true(); + }); + + it('should validate correct block ID (base58)', function () { + utils.isValidBlockId('GZXZvvLS3ZnuE4E9CxQJJ2ij5xeNsvUXdAKVrPCQKrPz').should.be.true(); + }); + + const invalidIdTestCases = [ + { + validator: 'isValidTransactionId', + cases: [ + { id: '0xff86b121181a43d03df52e8930785af3dda944ec87654cdba3a378ff518cd75b', description: 'hex format' }, + { id: 'BftEk3BeKUWTj9uzVGntd4Ka16QZG8hUnr6KsAb7q7b53t', description: 'wrong length' }, + { id: '0xabcdef123456', description: 'hex with prefix' }, + { id: '', description: 'empty string' }, + ], + }, + { + validator: 'isValidBlockId', + cases: [ + { id: '0x9ac6a0c313c4a0563a169dad29f1d018647683be54a314ed229a2693293dfc98', description: 'hex format' }, + { id: 'GZXZvvLS3ZnuE4E9CxQJJ2ij5xeNsvUXdAK56VrPCQKrPz', description: 'wrong length' }, + { id: '0xabcdef', description: 'hex with prefix' }, + { id: '', description: 'empty string' }, + ], + }, + ]; + + invalidIdTestCases.forEach(({ validator, cases }) => { + cases.forEach(({ id, description }) => { + it(`${validator} should reject ${description}`, function () { + utils[validator](id).should.be.false(); + }); + }); }); }); - describe('isValidBlockId', function () { - it('should validate correct block IDs', function () { - should.equal(true, utils.isValidBlockId('GZXZvvLS3ZnuE4E9CxQJJ2ij5xeNsvUXdAKVrPCQKrPz')); + describe('Signature Validation', function () { + it('should validate correct base64-encoded 64-byte signature', function () { + const validSignature = 'iXrcUjgQgpYUsa7O90KZicdTmIdJSjB99+tJW6l6wPCqI/lUTou6sQ2sLoZgC0n4qQKX+vFDz+lBIXl7J/ZgCg=='; + utils.isValidSignature(validSignature).should.be.true(); }); - it('should reject invalid block IDs', function () { - should.equal(false, utils.isValidBlockId('0x9ac6a0c313c4a0563a169dad29f1d018647683be54a314ed229a2693293dfc98')); - should.equal(false, utils.isValidBlockId('GZXZvvLS3ZnuE4E9CxQJJ2ij5xeNsvUXdAK56VrPCQKrPz')); // Wrong length - }); - - it('should reject hex strings', function () { - should.equal(false, utils.isValidBlockId('0xabcdef')); - }); + const invalidSignatureTestCases = [ + { sig: '0x9ac6a0c313c4a0563a169dad29f1d018647683be54a314ed229a2693293dfc98', description: 'hex format' }, + { sig: 'goppBTDgLuBbcU5tP90n3igvZGHmcE23HCoxLfdJwOCcbyztVh9r0TPacJRXmjZ6', description: 'wrong format' }, + { sig: 'dG9vU2hvcnQ=', description: 'too short (base64)' }, + { sig: 'not a base64 string!!!', description: 'invalid base64' }, + { sig: '', description: 'empty string' }, + ]; - it('should reject empty strings', function () { - should.equal(false, utils.isValidBlockId('')); + invalidSignatureTestCases.forEach(({ sig, description }) => { + it(`should reject ${description}`, function () { + utils.isValidSignature(sig).should.be.false(); + }); }); }); - describe('isValidSignature', function () { - it('should validate correct signatures', function () { - should.equal( - true, - utils.isValidSignature( - 'iXrcUjgQgpYUsa7O90KZicdTmIdJSjB99+tJW6l6wPCqI/lUTou6sQ2sLoZgC0n4qQKX+vFDz+lBIXl7J/ZgCg==' - ) - ); - }); - - it('should reject invalid signatures', function () { - should.equal(false, utils.isValidSignature('0x9ac6a0c313c4a0563a169dad29f1d018647683be54a314ed229a2693293dfc98')); - should.equal(false, utils.isValidSignature('goppBTDgLuBbcU5tP90n3igvZGHmcE23HCoxLfdJwOCcbyztVh9r0TPacJRXmjZ6')); - }); - - it('should reject signatures with incorrect length', function () { - should.equal(false, utils.isValidSignature('dG9vU2hvcnQ=')); // Too short base64 - }); - - it('should reject non-base64 strings', function () { - should.equal(false, utils.isValidSignature('not a base64 string!!!')); - should.equal(false, utils.isValidSignature('')); - }); - }); - - describe('isValidHex', function () { - it('should validate correct hex strings', function () { - should.equal(true, utils.isValidHex('0xabcdef', 6)); - should.equal(true, utils.isValidHex('0x123456', 6)); - should.equal(true, utils.isValidHex('0XABCDEF', 6)); - }); - - it('should reject hex strings with incorrect length', function () { - should.equal(false, utils.isValidHex('0xabcd', 6)); - should.equal(false, utils.isValidHex('0xabcdefgh', 6)); - }); - - it('should reject hex strings without prefix', function () { - should.equal(false, utils.isValidHex('abcdef', 6)); - }); - - it('should reject hex strings with non-hex characters', function () { - should.equal(false, utils.isValidHex('0xghijkl', 6)); - should.equal(false, utils.isValidHex('0xabcdeg', 6)); - }); - - it('should handle uppercase and lowercase', function () { - should.equal(true, utils.isValidHex('0xABCDEF', 6)); - should.equal(true, utils.isValidHex('0xabcdef', 6)); - should.equal(true, utils.isValidHex('0xAbCdEf', 6)); + describe('Hex String Validation', function () { + const hexTestCases = [ + { hex: '0xabcdef', length: 6, valid: true, description: 'lowercase' }, + { hex: '0x123456', length: 6, valid: true, description: 'numbers' }, + { hex: '0XABCDEF', length: 6, valid: true, description: 'uppercase prefix' }, + { hex: '0xABCDEF', length: 6, valid: true, description: 'uppercase' }, + { hex: '0xAbCdEf', length: 6, valid: true, description: 'mixed case' }, + { hex: '0xabcd', length: 6, valid: false, description: 'too short' }, + { hex: '0xabcdefgh', length: 6, valid: false, description: 'too long' }, + { hex: 'abcdef', length: 6, valid: false, description: 'no prefix' }, + { hex: '0xghijkl', length: 6, valid: false, description: 'non-hex chars' }, + { hex: '0xabcdeg', length: 6, valid: false, description: 'invalid char at end' }, + ]; + + hexTestCases.forEach(({ hex, length, valid, description }) => { + it(`should ${valid ? 'accept' : 'reject'} ${description}`, function () { + utils.isValidHex(hex, length).should.equal(valid); + }); }); }); - describe('getAddressFromPublicKey', function () { + describe('Address Derivation from Public Key', function () { it('should generate valid address from public key', function () { const address = utils.getAddressFromPublicKey(testData.sender.publicKey); should.exist(address); - should.equal(utils.isValidAddress(address), true); + utils.isValidAddress(address).should.be.true(); }); - it('should generate consistent addresses', function () { + it('should generate consistent addresses from same key', function () { const address1 = utils.getAddressFromPublicKey(testData.sender.publicKey); const address2 = utils.getAddressFromPublicKey(testData.sender.publicKey); - should.equal(address1, address2); + address1.should.equal(address2); }); it('should generate different addresses for different keys', function () { const address1 = utils.getAddressFromPublicKey(testData.sender.publicKey); const address2 = utils.getAddressFromPublicKey(testData.gasSponsor.publicKey); - should.notEqual(address1, address2); + address1.should.not.equal(address2); }); }); - describe('getBase64String', function () { - it('should convert Uint8Array to base64', function () { - const uint8Array = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" - const result = utils.getBase64String(uint8Array); - should.exist(result); - should.equal(typeof result, 'string'); - should.equal(result.length > 0, true); - }); - - it('should convert hex string to base64', function () { - const hexString = '48656c6c6f'; // "Hello" in hex - const result = utils.getBase64String(hexString); - should.exist(result); - should.equal(typeof result, 'string'); - should.equal(result.length > 0, true); - }); - - it('should handle empty Uint8Array', function () { - const emptyArray = new Uint8Array([]); - const result = utils.getBase64String(emptyArray); - should.exist(result); - should.equal(typeof result, 'string'); - }); + describe('Base64 String Conversion', function () { + const conversionTestCases = [ + { input: new Uint8Array([72, 101, 108, 108, 111]), description: 'Uint8Array ("Hello")' }, + { input: '48656c6c6f', description: 'hex string ("Hello")' }, + { input: new Uint8Array([]), description: 'empty Uint8Array' }, + { input: new Uint8Array(Buffer.from('Hello World')), description: 'Buffer as Uint8Array' }, + ]; - it('should handle Buffer conversion to base64', function () { - const buffer = Buffer.from('Hello World'); - const uint8Array = new Uint8Array(buffer); - const result = utils.getBase64String(uint8Array); - should.exist(result); - should.equal(typeof result, 'string'); - should.equal(result.length > 0, true); + conversionTestCases.forEach(({ input, description }) => { + it(`should convert ${description} to base64`, function () { + const result = utils.getBase64String(input); + should.exist(result); + should.equal(typeof result, 'string'); + }); }); - it('should consistently convert same input to same output', function () { + it('should produce consistent output for same input', function () { const uint8Array = new Uint8Array([1, 2, 3, 4, 5]); const result1 = utils.getBase64String(uint8Array); const result2 = utils.getBase64String(new Uint8Array([1, 2, 3, 4, 5])); - should.equal(result1, result2); + result1.should.equal(result2); }); }); - describe('isValidRawTransaction', function () { - it('should validate proper raw transactions', async function () { - const factory = new TransactionBuilderFactory(coins.get('tiota')); - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + describe('Raw Transaction Validation', function () { + it('should validate properly built transaction (base64)', async function () { + const txBuilder = createTransferBuilderWithGas(); const tx = await txBuilder.build(); const rawTx = await tx.toBroadcastFormat(); - should.equal(utils.isValidRawTransaction(rawTx), true); + utils.isValidRawTransaction(rawTx).should.be.true(); }); - it('should validate raw transactions as Uint8Array', async function () { - const factory = new TransactionBuilderFactory(coins.get('tiota')); - const txBuilder = factory.getTransferBuilder(); - txBuilder.sender(testData.sender.address); - txBuilder.recipients(testData.recipients); - txBuilder.paymentObjects(testData.paymentObjects); - txBuilder.gasData(testData.gasData); - + it('should validate transaction as Uint8Array', async function () { + const txBuilder = createTransferBuilderWithGas(); const tx = await txBuilder.build(); const rawTx = await tx.toBroadcastFormat(); const rawTxBytes = Buffer.from(rawTx, 'base64'); - should.equal(utils.isValidRawTransaction(rawTxBytes), true); + utils.isValidRawTransaction(rawTxBytes).should.be.true(); }); - it('should reject invalid raw transactions', function () { - should.equal(utils.isValidRawTransaction('invalidRawTx'), false); - should.equal(utils.isValidRawTransaction(''), false); - should.equal(utils.isValidRawTransaction('0x123456'), false); - }); - - it('should reject invalid base64 strings', function () { - should.equal(utils.isValidRawTransaction('not-base64!!!'), false); - }); + const invalidRawTxTestCases = [ + { tx: 'invalidRawTx', description: 'invalid string' }, + { tx: '', description: 'empty string' }, + { tx: '0x123456', description: 'hex format' }, + { tx: 'not-base64!!!', description: 'invalid base64' }, + { tx: Buffer.from('malformed data').toString('base64'), description: 'malformed data' }, + ]; - it('should reject malformed transaction data', function () { - const malformedBase64 = Buffer.from('malformed data').toString('base64'); - should.equal(utils.isValidRawTransaction(malformedBase64), false); + invalidRawTxTestCases.forEach(({ tx, description }) => { + it(`should reject ${description}`, function () { + utils.isValidRawTransaction(tx).should.be.false(); + }); }); }); });