From efa0607079da227789f411f72a8bd1bd1c3719d7 Mon Sep 17 00:00:00 2001 From: Kashif Jamil Date: Wed, 22 Apr 2026 16:32:10 +0530 Subject: [PATCH] Revert "feat(sdk-coin-kaspa): implement Kaspa SDK module" --- modules/sdk-coin-kaspa/.eslintignore | 5 - modules/sdk-coin-kaspa/.eslintrc.json | 7 - modules/sdk-coin-kaspa/.mocharc.yml | 8 - modules/sdk-coin-kaspa/README.md | 126 --------- modules/sdk-coin-kaspa/package.json | 52 ---- modules/sdk-coin-kaspa/src/index.ts | 4 - modules/sdk-coin-kaspa/src/kaspa.ts | 267 ------------------ modules/sdk-coin-kaspa/src/lib/constants.ts | 17 -- modules/sdk-coin-kaspa/src/lib/iface.ts | 127 --------- modules/sdk-coin-kaspa/src/lib/index.ts | 9 - modules/sdk-coin-kaspa/src/lib/keyPair.ts | 64 ----- modules/sdk-coin-kaspa/src/lib/sighash.ts | 226 --------------- modules/sdk-coin-kaspa/src/lib/transaction.ts | 148 ---------- .../src/lib/transactionBuilder.ts | 155 ---------- .../src/lib/transactionBuilderFactory.ts | 26 -- modules/sdk-coin-kaspa/src/lib/utils.ts | 257 ----------------- modules/sdk-coin-kaspa/src/register.ts | 7 - .../test/fixtures/kaspa.fixtures.ts | 99 ------- modules/sdk-coin-kaspa/test/unit/coin.test.ts | 223 --------------- .../sdk-coin-kaspa/test/unit/keyPair.test.ts | 62 ---- .../test/unit/transaction.test.ts | 230 --------------- .../test/unit/transactionBuilder.test.ts | 254 ----------------- .../test/unit/transactionFlow.test.ts | 128 --------- .../sdk-coin-kaspa/test/unit/utils.test.ts | 201 ------------- modules/sdk-coin-kaspa/tsconfig.json | 26 -- 25 files changed, 2728 deletions(-) delete mode 100644 modules/sdk-coin-kaspa/.eslintignore delete mode 100644 modules/sdk-coin-kaspa/.eslintrc.json delete mode 100644 modules/sdk-coin-kaspa/.mocharc.yml delete mode 100644 modules/sdk-coin-kaspa/README.md delete mode 100644 modules/sdk-coin-kaspa/package.json delete mode 100644 modules/sdk-coin-kaspa/src/index.ts delete mode 100644 modules/sdk-coin-kaspa/src/kaspa.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/constants.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/iface.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/index.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/keyPair.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/sighash.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/transaction.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/transactionBuilder.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/transactionBuilderFactory.ts delete mode 100644 modules/sdk-coin-kaspa/src/lib/utils.ts delete mode 100644 modules/sdk-coin-kaspa/src/register.ts delete mode 100644 modules/sdk-coin-kaspa/test/fixtures/kaspa.fixtures.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/coin.test.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/keyPair.test.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/transaction.test.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/transactionBuilder.test.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/transactionFlow.test.ts delete mode 100644 modules/sdk-coin-kaspa/test/unit/utils.test.ts delete mode 100644 modules/sdk-coin-kaspa/tsconfig.json diff --git a/modules/sdk-coin-kaspa/.eslintignore b/modules/sdk-coin-kaspa/.eslintignore deleted file mode 100644 index 190f83e0df..0000000000 --- a/modules/sdk-coin-kaspa/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -.idea -public -dist - diff --git a/modules/sdk-coin-kaspa/.eslintrc.json b/modules/sdk-coin-kaspa/.eslintrc.json deleted file mode 100644 index 8cc685ce4c..0000000000 --- a/modules/sdk-coin-kaspa/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../.eslintrc.json", - "rules": { - "@typescript-eslint/explicit-module-boundary-types": "error", - "indent": "off" - } -} diff --git a/modules/sdk-coin-kaspa/.mocharc.yml b/modules/sdk-coin-kaspa/.mocharc.yml deleted file mode 100644 index f499ec0a83..0000000000 --- a/modules/sdk-coin-kaspa/.mocharc.yml +++ /dev/null @@ -1,8 +0,0 @@ -require: 'tsx' -timeout: '60000' -reporter: 'min' -reporter-option: - - 'cdn=true' - - 'json=false' -exit: true -spec: ['test/unit/**/*.ts'] diff --git a/modules/sdk-coin-kaspa/README.md b/modules/sdk-coin-kaspa/README.md deleted file mode 100644 index ca075f3652..0000000000 --- a/modules/sdk-coin-kaspa/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# @bitgo/sdk-coin-kaspa - -BitGo's SDK module for the **Kaspa (KASPA)** blockchain. - -## Overview - -Kaspa is a proof-of-work blockchain based on the GHOSTDAG protocol (a generalization of Nakamoto consensus), enabling high block rates. This module implements Kaspa's UTXO model with **secp256k1 Schnorr signatures** (BIP-143-like sighash) and the custom **kaspa/kaspatest** bech32 address format. - -Supported coin identifiers: - -- `kaspa` — Kaspa mainnet -- `tkaspa` — Kaspa testnet - -## Features - -- Key pair generation (secp256k1) -- Address derivation (kaspa bech32 P2PK Schnorr) -- UTXO transaction building -- Schnorr signing and verification (Blake2b-256 sighash) -- TSS/MPC support (ECDSA algorithm) -- Full serialization round-trip (hex/JSON) - -## Installation - -```bash -yarn add @bitgo/sdk-coin-kaspa -``` - -## Usage - -### Register with BitGo SDK - -```typescript -import { register } from '@bitgo/sdk-coin-kaspa'; -register(bitgo); -``` - -### Key Pair - -```typescript -import { KeyPair } from '@bitgo/sdk-coin-kaspa'; - -// Generate a random key pair -const kp = new KeyPair(); -const { pub, prv } = kp.getKeys(); - -// Derive address -const mainnetAddress = kp.getAddress('mainnet'); -const testnetAddress = kp.getAddress('testnet'); -``` - -### Build and Sign a Transaction - -```typescript -import { TransactionBuilderFactory } from '@bitgo/sdk-coin-kaspa'; -import { coins } from '@bitgo/statics'; - -const factory = new TransactionBuilderFactory(coins.get('kaspa')); -const builder = factory.getBuilder(); - -builder - .addInput({ - transactionId: '', - transactionIndex: 0, - amount: '100000000', // 1 KASPA in sompi - scriptPublicKey: '', - sequence: '0', - sigOpCount: 1, - }) - .to('', '99998000') - .fee('2000'); - -const tx = await builder.build(); -tx.sign(Buffer.from(privateKeyHex, 'hex')); - -const broadcastPayload = tx.toBroadcastFormat(); // JSON string for RPC -``` - -## Module Structure - -``` -src/ -├── kaspa.ts # Kaspa mainnet coin class -├── tkaspa.ts # Kaspa testnet coin class -├── register.ts # SDK registration -├── index.ts -└── lib/ - ├── constants.ts # Chain constants (prefixes, decimals, fees) - ├── iface.ts # TypeScript interfaces - ├── keyPair.ts # secp256k1 key pair - ├── sighash.ts # Blake2b-256 Schnorr sighash - ├── transaction.ts # Transaction class (sign/verify/explain) - ├── transactionBuilder.ts # UTXO transaction builder - ├── transactionBuilderFactory.ts - ├── utils.ts # Address validation and encoding - └── index.ts -test/ -├── fixtures/ -│ ├── kaspa.fixtures.ts # Deterministic test vectors -│ └── kaspaFixtures.ts # Synthetic test fixtures -└── unit/ - ├── coin.test.ts - ├── keyPair.test.ts - ├── transaction.test.ts - ├── transactionBuilder.test.ts - ├── transactionFlow.test.ts - └── utils.test.ts -``` - -## Address Format - -Kaspa uses a custom cashaddr-like bech32 encoding: - -- Mainnet: `kaspa:` -- Testnet: `kaspatest:` -- Version byte `0` = Schnorr P2PK (x-only secp256k1 pubkey) - -## Signing - -Kaspa uses **Schnorr signatures over secp256k1** with a **Blake2b-256** sighash. The sighash preimage follows the Kaspa BIP-143-like specification. Each input is signed independently, producing a 65-byte signature: 64 bytes Schnorr + 1 byte sighash type. - -## References - -- [Kaspa Website](https://kaspa.org/) -- [Kaspa BIP-143-like SigHashes](https://github.com/kaspanet/docs/blob/main/Specs/BIP143-like%20SigHashes.md) -- [Kaspa RPC API](https://kaspa.aspectron.org/docs/) diff --git a/modules/sdk-coin-kaspa/package.json b/modules/sdk-coin-kaspa/package.json deleted file mode 100644 index f336c4c723..0000000000 --- a/modules/sdk-coin-kaspa/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@bitgo/sdk-coin-kaspa", - "version": "1.0.0", - "description": "BitGo's SDK coin library for Kaspa (KASPA)", - "main": "./dist/src/index.js", - "types": "./dist/src/index.d.ts", - "scripts": { - "build": "yarn tsc --build --incremental --verbose .", - "fmt": "prettier --write .", - "check-fmt": "prettier --check '**/*.{ts,js,json}'", - "clean": "rm -r ./dist", - "lint": "eslint --quiet .", - "prepare": "npm run build", - "test": "npm run coverage", - "coverage": "nyc -- npm run unit-test", - "unit-test": "mocha" - }, - "repository": { - "type": "git", - "url": "https://github.com/BitGo/BitGoJS.git", - "directory": "modules/sdk-coin-kaspa" - }, - "author": "BitGo SDK Team ", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "lint-staged": { - "*.{js,ts}": [ - "yarn prettier --write", - "yarn eslint --fix" - ] - }, - "publishConfig": { - "access": "public" - }, - "nyc": { - "extension": [ - ".ts" - ] - }, - "dependencies": { - "@bitgo/sdk-core": "^36.40.0", - "@bitgo/secp256k1": "^1.11.0", - "@bitgo/statics": "^58.35.0", - "bignumber.js": "9.0.0", - "blakejs": "^1.2.1" - }, - "files": [ - "dist" - ] -} diff --git a/modules/sdk-coin-kaspa/src/index.ts b/modules/sdk-coin-kaspa/src/index.ts deleted file mode 100644 index 35d22175ef..0000000000 --- a/modules/sdk-coin-kaspa/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './kaspa'; -export * from './lib'; -export * as KaspaLib from './lib'; -export * from './register'; diff --git a/modules/sdk-coin-kaspa/src/kaspa.ts b/modules/sdk-coin-kaspa/src/kaspa.ts deleted file mode 100644 index bddc3e1a39..0000000000 --- a/modules/sdk-coin-kaspa/src/kaspa.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics'; -import { - AuditDecryptedKeyParams, - BaseCoin, - BitGoBase, - InvalidAddressError, - InvalidTransactionError, - KeyPair as IKeyPair, - MethodNotImplementedError, - MPCAlgorithm, - MultisigType, - multisigTypes, - ParsedTransaction, - ParseTransactionOptions, - SignedTransaction, - UnexpectedAddressError, - VerifyAddressOptions, -} from '@bitgo/sdk-core'; -import * as KaspaLib from './lib'; -import { - KaspaExplainTransactionOptions, - KaspaSignTransactionOptions, - KaspaVerifyTransactionOptions, - TransactionExplanation, -} from './lib/iface'; -import { isValidKaspaAddress } from './lib/utils'; - -export class Kaspa extends BaseCoin { - protected readonly _staticsCoin: Readonly; - - constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { - super(bitgo); - - if (!staticsCoin) { - throw new Error('missing required constructor parameter staticsCoin'); - } - - this._staticsCoin = staticsCoin; - } - - static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { - return new Kaspa(bitgo, staticsCoin); - } - - getChain(): string { - return this._staticsCoin.name; - } - - getFamily(): CoinFamily { - return this._staticsCoin.family; - } - - getFullName(): string { - return this._staticsCoin.fullName; - } - - /** - * Return the base factor (sompi per KASPA). - * 1 KASPA = 100,000,000 sompi (8 decimal places) - */ - getBaseFactor(): string | number { - return Math.pow(10, this._staticsCoin.decimalPlaces); - } - - /** @inheritDoc */ - getDefaultMultisigType(): MultisigType { - return multisigTypes.onchain; - } - - /** - * Validate a Kaspa address. - */ - isValidAddress(address: string): boolean { - return isValidKaspaAddress(address); - } - - /** - * Validate a public key (secp256k1 compressed or uncompressed). - */ - isValidPub(pub: string): boolean { - try { - new KaspaLib.KeyPair({ pub }); - return true; - } catch { - return false; - } - } - - /** - * Validate a private key. - */ - isValidPrv(prv: string): boolean { - try { - new KaspaLib.KeyPair({ prv }); - return true; - } catch { - return false; - } - } - - /** - * Generate a Kaspa key pair. - */ - generateKeyPair(seed?: Buffer): IKeyPair { - const keyPair = seed ? new KaspaLib.KeyPair({ seed }) : new KaspaLib.KeyPair(); - const keys = keyPair.getKeys(); - - if (!keys.prv) { - throw new Error('Missing prv in key generation.'); - } - - return { - pub: keys.pub, - prv: keys.prv, - }; - } - - /** - * Check if address belongs to wallet by deriving from keychains. - */ - async isWalletAddress(params: VerifyAddressOptions): Promise { - const { address, keychains } = params; - - if (!this.isValidAddress(address)) { - throw new InvalidAddressError(`invalid address: ${address}`); - } - - if (!keychains || keychains.length !== 3) { - throw new Error('Invalid keychains'); - } - - const networkType = this._staticsCoin.network.type; - const derivedAddress = new KaspaLib.KeyPair({ pub: keychains[0].pub }).getAddress(networkType); - - if (derivedAddress !== address) { - throw new UnexpectedAddressError(`address validation failure: ${address} is not of this wallet`); - } - - return true; - } - - private getBuilder(): KaspaLib.TransactionBuilderFactory { - return new KaspaLib.TransactionBuilderFactory(coins.get(this.getChain())); - } - - /** - * Parse a Kaspa transaction from prebuild. - */ - async parseTransaction(params: ParseTransactionOptions): Promise { - const txHex = (params.txHex || (params as { halfSigned?: { txHex?: string } }).halfSigned?.txHex) as - | string - | undefined; - if (!txHex) { - return {}; - } - let tx: KaspaLib.Transaction; - try { - const txBuilder = this.getBuilder().from(txHex); - tx = (await txBuilder.build()) as KaspaLib.Transaction; - } catch (e) { - throw new InvalidTransactionError(`Invalid transaction: ${(e as Error).message}`); - } - const coin = this.getChain(); - return { - inputs: tx.txData.inputs.map((input) => ({ - amount: input.amount, - coin, - })), - outputs: tx.txData.outputs.map((output) => ({ - address: output.address, - amount: output.amount, - coin, - })), - }; - } - - /** - * Verify a Kaspa transaction against expected params. - */ - async verifyTransaction(params: KaspaVerifyTransactionOptions): Promise { - const txHex = params.txPrebuild?.txHex; - if (!txHex) { - throw new Error('missing required tx prebuild property txHex'); - } - - let tx: KaspaLib.Transaction; - try { - const txBuilder = this.getBuilder().from(txHex); - tx = (await txBuilder.build()) as KaspaLib.Transaction; - } catch (error) { - throw new InvalidTransactionError(`Invalid transaction: ${(error as Error).message}`); - } - - const explainedTx = tx.explainTransaction(); - - if (params.txParams.recipients) { - const recipientCount = params.txParams.recipients.length; - if (explainedTx.outputs.length < recipientCount) { - throw new Error(`Expected at least ${recipientCount} outputs, transaction had ${explainedTx.outputs.length}`); - } - } - - return true; - } - - /** - * Explain a Kaspa transaction. - */ - async explainTransaction(params: KaspaExplainTransactionOptions): Promise { - const txHex = params.txHex ?? params?.halfSigned?.txHex; - if (!txHex) { - throw new Error('missing transaction hex'); - } - try { - const txBuilder = this.getBuilder().from(txHex); - const tx = (await txBuilder.build()) as KaspaLib.Transaction; - return tx.explainTransaction(); - } catch (e) { - throw new InvalidTransactionError(`Invalid transaction: ${(e as Error).message}`); - } - } - - /** - * Sign a Kaspa transaction using secp256k1 Schnorr signatures. - */ - async signTransaction(params: KaspaSignTransactionOptions): Promise { - const txHex = params.txPrebuild.txHex; - if (!txHex) { - throw new InvalidTransactionError('missing txHex in txPrebuild'); - } - - const txBuilder = this.getBuilder().from(txHex); - const tx = (await txBuilder.build()) as KaspaLib.Transaction; - - if (params.prv) { - const privKeyHex = params.prv.slice(0, 64); - const privKeyBuffer = Buffer.from(privKeyHex, 'hex'); - tx.sign(privKeyBuffer); - } - - const signedHex = tx.toHex(); - const inputCount = tx.txData.inputs.length; - const sigCount = tx.signature.filter((s) => s.length > 0).length; - - return inputCount > 0 && sigCount >= inputCount ? { txHex: signedHex } : { halfSigned: { txHex: signedHex } }; - } - - async signMessage(key: IKeyPair, message: string | Buffer): Promise { - throw new MethodNotImplementedError(); - } - - /** @inheritDoc */ - auditDecryptedKey(params: AuditDecryptedKeyParams): void { - throw new MethodNotImplementedError(); - } - - /** - * MPC support: Kaspa uses secp256k1 (Schnorr variant). - */ - supportsTss(): boolean { - return true; - } - - getMPCAlgorithm(): MPCAlgorithm { - return 'ecdsa'; - } -} diff --git a/modules/sdk-coin-kaspa/src/lib/constants.ts b/modules/sdk-coin-kaspa/src/lib/constants.ts deleted file mode 100644 index 747e1edf9c..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Kaspa (KASPA) Constants - * - * References: - * - https://kaspa.org/ - * - https://kaspa.aspectron.org/docs/ - */ - -// Address format -export const MAINNET_PREFIX = 'kaspa'; -export const TESTNET_PREFIX = 'kaspatest'; - -// Default transaction fee (minimum relay fee in sompi) -export const DEFAULT_FEE = '1000'; // 0.00001 KASPA minimum - -// Kaspa transaction version -export const TX_VERSION = 0; diff --git a/modules/sdk-coin-kaspa/src/lib/iface.ts b/modules/sdk-coin-kaspa/src/lib/iface.ts deleted file mode 100644 index 3e26d3a223..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/iface.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - TransactionExplanation as BaseTransactionExplanation, - TransactionPrebuild as BaseTransactionPrebuild, - SignTransactionOptions, - TransactionParams, - TransactionRecipient, - TransactionType, - VerifyTransactionOptions, -} from '@bitgo/sdk-core'; - -/** - * Kaspa UTXO input (reference to a previous transaction output) - */ -export interface KaspaUtxoInput { - /** Previous transaction ID (hex string) */ - transactionId: string; - /** Output index in the previous transaction */ - transactionIndex: number; - /** Amount in sompi */ - amount: string; - /** Script public key of the UTXO */ - scriptPublicKey: string; - /** Sequence number */ - sequence?: string; - /** Signature operation count */ - sigOpCount?: number; - /** Signature script — hex-encoded (65 bytes: 64-byte Schnorr sig + 1-byte sighash type) */ - signatureScript?: string; -} - -/** - * Kaspa transaction output - */ -export interface KaspaTransactionOutput { - /** Recipient address */ - address: string; - /** Amount in sompi */ - amount: string; - /** Script public key */ - scriptPublicKey?: string; -} - -/** - * Kaspa transaction data structure - */ -export interface KaspaTransactionData { - /** Transaction version */ - version: number; - /** Transaction inputs (UTXOs being spent) */ - inputs: KaspaUtxoInput[]; - /** Transaction outputs */ - outputs: KaspaTransactionOutput[]; - /** Lock time */ - lockTime?: string; - /** Subnetwork ID (all-zeros for native KASPA transfers) */ - subnetworkId?: string; - /** Transaction payload (empty for native transfers) */ - payload?: string; - /** Fee in sompi */ - fee?: string; - /** Transaction ID (computed) */ - id?: string; -} - -/** - * Kaspa transaction explanation for users - */ -export interface TransactionExplanation extends BaseTransactionExplanation { - type: TransactionType; - inputs: KaspaUtxoInput[]; -} - -/** - * Kaspa transaction prebuild for signing - */ -export interface TransactionPrebuild extends BaseTransactionPrebuild { - /** Serialized transaction as hex or JSON string */ - txHex: string; - txInfo: KaspaTxInfo; - source: string; -} - -export interface KaspaTxInfo { - recipients: TransactionRecipient[]; - from: string; - utxos?: KaspaUtxoInput[]; -} - -/** - * Kaspa sign transaction options - */ -export interface KaspaSignTransactionOptions extends SignTransactionOptions { - txPrebuild: TransactionPrebuild; - prv: string; -} - -/** - * Kaspa transaction params - */ -export interface KaspaTransactionParams extends TransactionParams { - type?: string; - unspents?: KaspaUtxoInput[]; -} - -/** - * Kaspa explain transaction options - */ -export interface KaspaExplainTransactionOptions { - txHex?: string; - halfSigned?: { - txHex: string; - }; -} - -/** - * Kaspa verify transaction options - */ -export interface KaspaVerifyTransactionOptions extends VerifyTransactionOptions { - txParams: KaspaTransactionParams; -} - -/** - * Kaspa transaction fee info - */ -export interface KaspaTransactionFee { - fee: string; -} diff --git a/modules/sdk-coin-kaspa/src/lib/index.ts b/modules/sdk-coin-kaspa/src/lib/index.ts deleted file mode 100644 index ac56d9988b..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { KeyPair } from './keyPair'; -export { Transaction } from './transaction'; -export { TransactionBuilder } from './transactionBuilder'; -export { TransactionBuilderFactory } from './transactionBuilderFactory'; -export * from './iface'; -export * from './constants'; -export * from './utils'; -export * from './sighash'; -export { default as Utils, Utils as KaspaUtils } from './utils'; diff --git a/modules/sdk-coin-kaspa/src/lib/keyPair.ts b/modules/sdk-coin-kaspa/src/lib/keyPair.ts deleted file mode 100644 index 0ea87c68c6..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/keyPair.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - DefaultKeys, - isPrivateKey, - isPublicKey, - isSeed, - KeyPairOptions, - Secp256k1ExtendedKeyPair, -} from '@bitgo/sdk-core'; -import { bip32 } from '@bitgo/secp256k1'; -import { randomBytes } from 'crypto'; -import { pubKeyToKaspaAddress } from './utils'; -import { MAINNET_PREFIX, TESTNET_PREFIX } from './constants'; - -const DEFAULT_SEED_SIZE_BYTES = 16; - -export class KeyPair extends Secp256k1ExtendedKeyPair { - /** - * Public constructor. By default, creates a key pair with a random master seed. - * - * @param { KeyPairOptions } source Either a master seed, a private key, or a public key - */ - constructor(source?: KeyPairOptions) { - super(source); - if (!source) { - const seed = randomBytes(DEFAULT_SEED_SIZE_BYTES); - this.hdNode = bip32.fromSeed(seed); - } else if (isSeed(source)) { - this.hdNode = bip32.fromSeed(source.seed); - } else if (isPrivateKey(source)) { - this.recordKeysFromPrivateKey(source.prv); - } else if (isPublicKey(source)) { - this.recordKeysFromPublicKey(source.pub); - } else { - throw new Error('Invalid key pair options'); - } - - if (this.hdNode) { - this.keyPair = Secp256k1ExtendedKeyPair.toKeyPair(this.hdNode); - } - } - - /** - * Default keys format is a pair of hex keys. - * - * @returns { DefaultKeys } The keys in the defined format - */ - getKeys(): DefaultKeys { - return { - pub: this.getPublicKey({ compressed: true }).toString('hex'), - prv: this.getPrivateKey()?.toString('hex'), - }; - } - - /** - * Get a Kaspa address from this key pair. - * - * @returns {string} The bech32-encoded Kaspa address - */ - getAddress(network = 'mainnet'): string { - const hrp = network === 'testnet' ? TESTNET_PREFIX : MAINNET_PREFIX; - const compressedPub = this.getPublicKey({ compressed: true }); - return pubKeyToKaspaAddress(compressedPub, hrp); - } -} diff --git a/modules/sdk-coin-kaspa/src/lib/sighash.ts b/modules/sdk-coin-kaspa/src/lib/sighash.ts deleted file mode 100644 index d326c27465..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/sighash.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Kaspa transaction sighash computation. - * - * BIP-143-like scheme using Blake2b-256. All integer fields are little-endian. - * - * Reference (authoritative implementation): - * https://github.com/kaspanet/rusty-kaspa/blob/master/consensus/core/src/hashing/sighash.rs - */ -import { blake2b } from 'blakejs'; -import { KaspaTransactionData, KaspaUtxoInput, KaspaTransactionOutput } from './iface'; - -// SigHash type flags -export const SIGHASH_ALL = 0x01; -export const SIGHASH_NONE = 0x02; -export const SIGHASH_SINGLE = 0x04; -export const SIGHASH_ANYONECANPAY = 0x80; - -// Script constants -export const OP_CHECKSIG_SCHNORR = 0xab; // Kaspa Schnorr checksig opcode -export const SCRIPT_PUBLIC_KEY_VERSION = 0; // Standard P2PK version - -function blake2b256(data: Buffer): Buffer { - return Buffer.from(blake2b(data, undefined, 32)); -} - -/** - * Build P2PK Schnorr scriptPublicKey from a 32-byte x-only public key. - * Format: DATA_32(0x20) + xOnlyPubKey(32 bytes) + OP_CHECKSIG_SCHNORR(0xAB) - */ -export function buildP2PKScriptPublicKey(xOnlyPubKey: Buffer): Buffer { - if (xOnlyPubKey.length !== 32) { - throw new Error(`Expected 32-byte x-only pubkey, got ${xOnlyPubKey.length}`); - } - return Buffer.concat([Buffer.from([0x20]), xOnlyPubKey, Buffer.from([OP_CHECKSIG_SCHNORR])]); -} - -/** - * Derive x-only public key from 33-byte compressed public key. - */ -export function compressedToXOnly(compressedPubKey: Buffer): Buffer { - if (compressedPubKey.length !== 33) { - throw new Error(`Expected 33-byte compressed pubkey, got ${compressedPubKey.length}`); - } - return compressedPubKey.slice(1); // drop the 02/03 prefix byte -} - -// --- Intermediate hash helpers --- - -function hashPreviousOutputs(inputs: KaspaUtxoInput[]): Buffer { - const parts = inputs.map((inp) => { - const buf = Buffer.alloc(36); - Buffer.from(inp.transactionId, 'hex').copy(buf, 0); - buf.writeUInt32LE(inp.transactionIndex, 32); - return buf; - }); - return blake2b256(Buffer.concat(parts)); -} - -function hashSequences(inputs: KaspaUtxoInput[]): Buffer { - const buf = Buffer.alloc(inputs.length * 8); - inputs.forEach((inp, i) => { - buf.writeBigUInt64LE(BigInt(inp.sequence || '0'), i * 8); - }); - return blake2b256(buf); -} - -function hashSigOpCounts(inputs: KaspaUtxoInput[]): Buffer { - const bytes = inputs.map((inp) => inp.sigOpCount ?? 1); - return blake2b256(Buffer.from(bytes)); -} - -function serializeOutput(output: KaspaTransactionOutput): Buffer { - const scriptBytes = Buffer.from(output.scriptPublicKey || '', 'hex'); - const spkVersion = 0; // standard P2PK - // value (u64 LE, 8) + spk_version (u16 LE, 2) + script_length (u64 LE, 8) + script - const buf = Buffer.alloc(8 + 2 + 8 + scriptBytes.length); - let offset = 0; - buf.writeBigUInt64LE(BigInt(output.amount), offset); - offset += 8; - buf.writeUInt16LE(spkVersion, offset); - offset += 2; - buf.writeBigUInt64LE(BigInt(scriptBytes.length), offset); - offset += 8; - scriptBytes.copy(buf, offset); - return buf; -} - -function hashOutputs(tx: KaspaTransactionData, inputIndex: number, sigHashType: number): Buffer { - const baseType = sigHashType & 0x1f; - if (baseType === SIGHASH_NONE) { - return Buffer.alloc(32); // zero hash - } - if (baseType === SIGHASH_SINGLE) { - if (inputIndex >= tx.outputs.length) { - return Buffer.alloc(32); - } - return blake2b256(serializeOutput(tx.outputs[inputIndex])); - } - // SIGHASH_ALL - const parts = tx.outputs.map(serializeOutput); - return blake2b256(Buffer.concat(parts)); -} - -function hashPayload(tx: KaspaTransactionData): Buffer { - const subnetId = Buffer.from(tx.subnetworkId || '0000000000000000000000000000000000000000', 'hex'); - // If subnetwork is native (all zeros), payloadHash is zero - if (subnetId.every((b) => b === 0)) { - return Buffer.alloc(32); - } - return blake2b256(Buffer.from(tx.payload || '', 'hex')); -} - -/** - * Compute the Kaspa sighash for a specific input. - * - * @param tx Full transaction data - * @param inputIndex Index of the input being signed - * @param sigHashType SigHash type flags (use SIGHASH_ALL = 0x01 for standard) - */ -export function computeKaspaSigningHash( - tx: KaspaTransactionData, - inputIndex: number, - sigHashType: number = SIGHASH_ALL -): Buffer { - const anyoneCanPay = !!(sigHashType & SIGHASH_ANYONECANPAY); - const baseType = sigHashType & 0x1f; - - const input = tx.inputs[inputIndex]; - if (!input) { - throw new Error(`Input index ${inputIndex} out of range`); - } - - // Conditional intermediate hashes - const prevOutputsHash = anyoneCanPay ? Buffer.alloc(32) : hashPreviousOutputs(tx.inputs); - const seqHash = - anyoneCanPay || baseType === SIGHASH_SINGLE || baseType === SIGHASH_NONE - ? Buffer.alloc(32) - : hashSequences(tx.inputs); - const sigOpHash = anyoneCanPay ? Buffer.alloc(32) : hashSigOpCounts(tx.inputs); - const outsHash = hashOutputs(tx, inputIndex, sigHashType); - const payloadHash = hashPayload(tx); - - // Parse the current input's script public key - const scriptBytes = Buffer.from(input.scriptPublicKey || '', 'hex'); - const spkVersion = 0; // standard P2PK - const subnetId = Buffer.from(tx.subnetworkId || '0000000000000000000000000000000000000000', 'hex'); - - // Build the preimage - const fixedSize = 2 + 32 + 32 + 32 + 32 + 4 + 2 + 8 + 8 + 8 + 1 + 32 + 8 + 20 + 8 + 32 + 1; - const preimage = Buffer.alloc(fixedSize + scriptBytes.length); - let offset = 0; - - // 1. version - preimage.writeUInt16LE(tx.version ?? 0, offset); - offset += 2; - - // 2. previousOutputsHash - prevOutputsHash.copy(preimage, offset); - offset += 32; - - // 3. sequencesHash - seqHash.copy(preimage, offset); - offset += 32; - - // 4. sigOpCountsHash - sigOpHash.copy(preimage, offset); - offset += 32; - - // 5. current input's previous outpoint txId - Buffer.from(input.transactionId, 'hex').copy(preimage, offset); - offset += 32; - - // 6. current input's previous outpoint index - preimage.writeUInt32LE(input.transactionIndex, offset); - offset += 4; - - // 7. scriptPublicKey version - preimage.writeUInt16LE(spkVersion, offset); - offset += 2; - - // 8. scriptPublicKey length (u64 LE) - preimage.writeBigUInt64LE(BigInt(scriptBytes.length), offset); - offset += 8; - - // 9. scriptPublicKey bytes - scriptBytes.copy(preimage, offset); - offset += scriptBytes.length; - - // 10. value (amount in sompi, u64 LE) - preimage.writeBigUInt64LE(BigInt(input.amount), offset); - offset += 8; - - // 11. sequence (u64 LE) - preimage.writeBigUInt64LE(BigInt(input.sequence || '0'), offset); - offset += 8; - - // 12. sigOpCount (u8) - preimage.writeUInt8(input.sigOpCount ?? 1, offset); - offset += 1; - - // 13. outputsHash - outsHash.copy(preimage, offset); - offset += 32; - - // 14. locktime (u64 LE) - preimage.writeBigUInt64LE(BigInt(tx.lockTime || '0'), offset); - offset += 8; - - // 15. subnetworkId (20 bytes) - subnetId.copy(preimage, offset); - offset += 20; - - // 16. gas (u64 LE) — always 0 for native KASPA - preimage.writeBigUInt64LE(0n, offset); - offset += 8; - - // 17. payloadHash - payloadHash.copy(preimage, offset); - offset += 32; - - // 18. sigHashType (u8) - preimage.writeUInt8(sigHashType, offset); - offset += 1; - - return blake2b256(preimage.slice(0, offset)); -} diff --git a/modules/sdk-coin-kaspa/src/lib/transaction.ts b/modules/sdk-coin-kaspa/src/lib/transaction.ts deleted file mode 100644 index ea7c10958e..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/transaction.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { BaseKey, BaseTransaction, TransactionType } from '@bitgo/sdk-core'; -import { ecc } from '@bitgo/secp256k1'; -import { KaspaTransactionData, TransactionExplanation } from './iface'; -import { computeKaspaSigningHash, SIGHASH_ALL } from './sighash'; - -export class Transaction extends BaseTransaction { - protected _txData: KaspaTransactionData; - - constructor(coin: string, txData?: KaspaTransactionData) { - super({ coin } as any); - this._txData = txData || { - version: 0, - inputs: [], - outputs: [], - }; - } - - /** @inheritDoc */ - get id(): string { - return this._txData.id || ''; - } - - get txData(): KaspaTransactionData { - return this._txData; - } - - /** - * Sign all inputs with the given private key using Schnorr signatures. - * - * @param privateKey 32-byte private key buffer - * @param sigHashType SigHash type (default: SIGHASH_ALL = 0x01) - */ - sign(privateKey: Buffer, sigHashType: number = SIGHASH_ALL): void { - if (privateKey.length !== 32) { - throw new Error(`Expected 32-byte private key, got ${privateKey.length}`); - } - for (let i = 0; i < this._txData.inputs.length; i++) { - const sigHash = computeKaspaSigningHash(this._txData, i, sigHashType); - const sig = ecc.signSchnorr(sigHash, privateKey); - // 65-byte signature: 64-byte Schnorr sig + 1-byte sighash type - const sigWithType = Buffer.concat([Buffer.from(sig), Buffer.from([sigHashType])]); - this._txData.inputs[i].signatureScript = sigWithType.toString('hex'); - } - } - - /** - * Verify that a Schnorr signature on a specific input is valid. - * - * @param publicKey 33-byte compressed public key (or 32-byte x-only) - * @param inputIndex Index of the input to verify - * @param sigHashType SigHash type used when signing - */ - verifySignature(publicKey: Buffer, inputIndex: number, sigHashType: number = SIGHASH_ALL): boolean { - const input = this._txData.inputs[inputIndex]; - if (!input?.signatureScript) { - return false; - } - const sigBytes = Buffer.from(input.signatureScript, 'hex'); - if (sigBytes.length < 65) { - return false; - } - const sig = sigBytes.slice(0, 64); - const sigHash = computeKaspaSigningHash(this._txData, inputIndex, sigHashType); - // Accept 33-byte compressed or 32-byte x-only - const xOnlyPub = publicKey.length === 33 ? publicKey.slice(1) : publicKey; - return ecc.verifySchnorr(sigHash, xOnlyPub, sig); - } - - /** - * Explain the transaction in a human-readable format. - */ - explainTransaction(): TransactionExplanation { - let totalIn = BigInt(0); - let totalOut = BigInt(0); - - for (const input of this._txData.inputs) { - totalIn += BigInt(input.amount); - } - for (const output of this._txData.outputs) { - totalOut += BigInt(output.amount); - } - - const fee = (totalIn - totalOut).toString(); - - const outputs = this._txData.outputs.map((o) => ({ - address: o.address, - amount: o.amount, - })); - - return { - id: this._txData.id || '', - type: TransactionType.Send, - outputs, - outputAmount: totalOut.toString(), - changeOutputs: [], - changeAmount: '0', - fee: { fee: this._txData.fee || fee }, - inputs: this._txData.inputs, - }; - } - - /** - * Serialize the transaction to a JSON string (broadcast format). - */ - toBroadcastFormat(): string { - return JSON.stringify(this._txData); - } - - /** - * Serialize transaction to hex (JSON-encoded then hex-encoded). - */ - toHex(): string { - return Buffer.from(this.toBroadcastFormat()).toString('hex'); - } - - /** - * Return transaction data as a plain object. - */ - toJson(): KaspaTransactionData { - return { ...this._txData }; - } - - /** - * Deserialize from hex. - */ - static fromHex(coin: string, hex: string): Transaction { - const json = JSON.parse(Buffer.from(hex, 'hex').toString()); - return new Transaction(coin, json); - } - - /** - * Deserialize from JSON string or object. - */ - static fromJson(coin: string, data: string | KaspaTransactionData): Transaction { - const txData: KaspaTransactionData = typeof data === 'string' ? JSON.parse(data) : data; - return new Transaction(coin, txData); - } - - /** @inheritDoc */ - get signature(): string[] { - return this._txData.inputs.map((i) => i.signatureScript || ''); - } - - /** @inheritDoc */ - canSign(_key: BaseKey): boolean { - return true; - } -} diff --git a/modules/sdk-coin-kaspa/src/lib/transactionBuilder.ts b/modules/sdk-coin-kaspa/src/lib/transactionBuilder.ts deleted file mode 100644 index 37679a440d..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/transactionBuilder.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { BaseTransactionBuilder, BaseTransaction, BaseKey, SigningError } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import BigNumber from 'bignumber.js'; -import { Transaction } from './transaction'; -import { KaspaTransactionData, KaspaUtxoInput, KaspaTransactionOutput } from './iface'; -import { isValidKaspaAddress } from './utils'; -import { KeyPair } from './keyPair'; -import { DEFAULT_FEE, TX_VERSION } from './constants'; - -export class TransactionBuilder extends BaseTransactionBuilder { - protected _transaction: Transaction; - protected _inputs: KaspaUtxoInput[] = []; - protected _outputs: KaspaTransactionOutput[] = []; - protected _fee: string = DEFAULT_FEE; - protected _fromAddress = ''; - - constructor(coinConfig: Readonly) { - super(coinConfig); - this._transaction = new Transaction(coinConfig.name); - } - - /** @inheritDoc */ - protected get transaction(): BaseTransaction { - return this._transaction; - } - - /** @inheritDoc */ - protected set transaction(tx: BaseTransaction) { - this._transaction = tx as Transaction; - } - - /** - * Set the sender address. - */ - sender(address: string): this { - if (!isValidKaspaAddress(address)) { - throw new Error(`Invalid Kaspa address: ${address}`); - } - this._fromAddress = address; - return this; - } - - /** - * Add a recipient output. - */ - to(address: string, amount: string): this { - if (!isValidKaspaAddress(address)) { - throw new Error(`Invalid Kaspa recipient address: ${address}`); - } - this._outputs.push({ address, amount }); - return this; - } - - /** - * Set transaction fee in sompi. - */ - fee(fee: string): this { - this._fee = fee; - return this; - } - - /** - * Add a UTXO input. - */ - addInput(utxo: KaspaUtxoInput): this { - if (!utxo.transactionId || utxo.transactionIndex === undefined) { - throw new Error('Invalid UTXO: missing transactionId or transactionIndex'); - } - this._inputs.push(utxo); - return this; - } - - /** - * Add multiple UTXO inputs. - */ - addInputs(utxos: KaspaUtxoInput[]): this { - utxos.forEach((u) => this.addInput(u)); - return this; - } - - /** @inheritDoc */ - protected fromImplementation(rawTransaction: string): Transaction { - const tx = Transaction.fromHex((this as any)._coinConfig?.name || 'kaspa', rawTransaction); - this._transaction = tx; - this._inputs = tx.txData.inputs || []; - this._outputs = tx.txData.outputs || []; - this._fee = tx.txData.fee || DEFAULT_FEE; - return tx; - } - - /** @inheritDoc */ - protected async buildImplementation(): Promise { - const txData: KaspaTransactionData = { - version: TX_VERSION, - inputs: this._inputs, - outputs: this._outputs, - fee: this._fee, - lockTime: '0', - subnetworkId: '0000000000000000000000000000000000000000', - payload: '', - }; - - this._transaction = new Transaction((this as any)._coinConfig?.name || 'kaspa', txData); - return this._transaction; - } - - /** @inheritDoc */ - protected signImplementation({ key }: BaseKey): BaseTransaction { - const keyPair = new KeyPair({ prv: key }); - const privateKey = keyPair.getKeys().prv; - if (!privateKey) { - throw new SigningError('Missing private key'); - } - this._transaction.sign(Buffer.from(privateKey, 'hex')); - return this._transaction; - } - - /** @inheritDoc */ - validateTransaction(_transaction?: BaseTransaction): void { - if (this._inputs.length === 0) { - throw new Error('At least one UTXO input is required'); - } - if (this._outputs.length === 0) { - throw new Error('At least one output is required'); - } - } - - /** @inheritDoc */ - validateKey(key: { key: string }): void { - // Key validation handled in KeyPair - } - - /** @inheritDoc */ - validateAddress(address: { address: string }): void { - if (!isValidKaspaAddress(address.address)) { - throw new Error(`Invalid Kaspa address: ${address.address}`); - } - } - - /** @inheritDoc */ - validateRawTransaction(rawTransaction: string): void { - try { - Transaction.fromHex((this as any)._coinConfig?.name || 'kaspa', rawTransaction); - } catch { - throw new Error('Invalid raw Kaspa transaction'); - } - } - - /** @inheritDoc */ - validateValue(value: BigNumber): void { - if (value.isLessThan(0)) { - throw new Error('Transaction value cannot be negative'); - } - } -} diff --git a/modules/sdk-coin-kaspa/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-kaspa/src/lib/transactionBuilderFactory.ts deleted file mode 100644 index 2e59e3f671..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/transactionBuilderFactory.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; -import { TransactionBuilder } from './transactionBuilder'; - -export class TransactionBuilderFactory { - protected _coinConfig: Readonly; - - constructor(coinConfig: Readonly) { - this._coinConfig = coinConfig; - } - - /** - * Get a base transaction builder for Kaspa transfers. - */ - getBuilder(): TransactionBuilder { - return new TransactionBuilder(this._coinConfig); - } - - /** - * Reconstruct a transaction builder from a raw transaction hex. - */ - from(rawTransaction: string): TransactionBuilder { - const builder = this.getBuilder(); - builder.from(rawTransaction); - return builder; - } -} diff --git a/modules/sdk-coin-kaspa/src/lib/utils.ts b/modules/sdk-coin-kaspa/src/lib/utils.ts deleted file mode 100644 index 85250e62ad..0000000000 --- a/modules/sdk-coin-kaspa/src/lib/utils.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { BaseUtils, isValidXprv, isValidXpub } from '@bitgo/sdk-core'; -import { MAINNET_PREFIX, TESTNET_PREFIX } from './constants'; - -// Kaspa address encoding uses a bech32-like scheme with ':' as separator -// and a custom checksum polynomial (same as Bitcoin Cash cashaddr). - -const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; -const CHARSET_REVERSE: Record = {}; -for (let i = 0; i < CHARSET.length; i++) { - CHARSET_REVERSE[CHARSET[i]] = i; -} - -const GENERATOR = [ - BigInt('0x98f2bc8e61'), - BigInt('0x79b76d99e2'), - BigInt('0xf33e5fb3c4'), - BigInt('0xae2eabe2a8'), - BigInt('0x1e4f43e470'), -]; - -function polymod(values: number[]): bigint { - let c = 1n; - for (const d of values) { - const c0 = c >> 35n; - c = ((c & BigInt('0x07ffffffff')) << 5n) ^ BigInt(d); - for (let i = 0; i < 5; i++) { - if ((c0 >> BigInt(i)) & 1n) { - c ^= GENERATOR[i]; - } - } - } - return c ^ 1n; -} - -function prefixExpand(prefix: string): number[] { - return [...prefix].map((c) => c.charCodeAt(0) & 0x1f).concat([0]); -} - -function createChecksum(prefix: string, data: number[]): number[] { - const values = prefixExpand(prefix).concat(data).concat([0, 0, 0, 0, 0, 0, 0, 0]); - const mod = polymod(values); - const ret: number[] = []; - for (let i = 7; i >= 0; i--) { - ret.push(Number((mod >> BigInt(5 * i)) & 31n)); - } - return ret; -} - -function verifyChecksum(prefix: string, data: number[]): boolean { - const values = prefixExpand(prefix).concat(data); - return polymod(values) === 0n; -} - -function convertBits(data: Buffer | Uint8Array, from: number, to: number, pad: boolean): number[] { - let acc = 0; - let bits = 0; - const ret: number[] = []; - const maxv = (1 << to) - 1; - for (const value of data) { - acc = (acc << from) | value; - bits += from; - while (bits >= to) { - bits -= to; - ret.push((acc >> bits) & maxv); - } - } - if (pad && bits > 0) { - ret.push((acc << (to - bits)) & maxv); - } - return ret; -} - -/** - * Encode data into a Kaspa bech32-like address. - */ -function kaspaEncode(prefix: string, data: number[]): string { - const checksum = createChecksum(prefix, data); - return ( - prefix + - ':' + - data - .concat(checksum) - .map((d) => CHARSET[d]) - .join('') - ); -} - -/** - * Decode a Kaspa bech32-like address. - * Returns { prefix, data } or throws on error. - */ -function kaspacDecode(address: string): { prefix: string; data: number[] } { - const colonIdx = address.lastIndexOf(':'); - if (colonIdx < 1) { - throw new Error('Missing prefix separator'); - } - const prefix = address.slice(0, colonIdx).toLowerCase(); - const encoded = address.slice(colonIdx + 1).toLowerCase(); - - const data: number[] = []; - for (const c of encoded) { - const val = CHARSET_REVERSE[c]; - if (val === undefined) { - throw new Error(`Invalid character: ${c}`); - } - data.push(val); - } - - if (!verifyChecksum(prefix, data)) { - throw new Error('Invalid checksum'); - } - - return { prefix, data: data.slice(0, -8) }; -} - -/** - * Validates a Kaspa address (mainnet or testnet) - */ -export function isValidKaspaAddress(address: string): boolean { - if (!address || typeof address !== 'string') { - return false; - } - - const colonIdx = address.lastIndexOf(':'); - if (colonIdx < 1) { - return false; - } - - const prefix = address.slice(0, colonIdx).toLowerCase(); - if (prefix !== MAINNET_PREFIX && prefix !== TESTNET_PREFIX) { - return false; - } - - try { - const decoded = kaspacDecode(address); - return decoded.data.length > 0; - } catch { - return false; - } -} - -/** - * Validates a Kaspa mainnet address - */ -export function isValidMainnetAddress(address: string): boolean { - return isValidKaspaAddress(address) && address.toLowerCase().startsWith(MAINNET_PREFIX + ':'); -} - -/** - * Validates a Kaspa testnet address - */ -export function isValidTestnetAddress(address: string): boolean { - return isValidKaspaAddress(address) && address.toLowerCase().startsWith(TESTNET_PREFIX + ':'); -} - -/** - * Derive a Kaspa P2PK (Schnorr) address from a compressed secp256k1 public key. - * - * @param compressedPubKey - 33-byte compressed secp256k1 public key (hex string or Buffer) - * @param hrp - human-readable part ('kaspa' or 'kaspatest') - */ -export function pubKeyToKaspaAddress(compressedPubKey: string | Buffer, hrp: string): string { - const pubKeyBytes = Buffer.isBuffer(compressedPubKey) - ? compressedPubKey - : Buffer.from(compressedPubKey as string, 'hex'); - - if (pubKeyBytes.length !== 33) { - throw new Error(`Expected 33-byte compressed public key, got ${pubKeyBytes.length}`); - } - - // X-only public key: drop the prefix byte (02 or 03), keep 32-byte x-coordinate - const xOnlyPubKey = pubKeyBytes.slice(1); - - // Kaspa P2PK address: - // - version nibble: 0 (Schnorr secp256k1 P2PK) - // - payload: x-only public key (32 bytes) - const versionByte = 0; - const payload = Buffer.concat([Buffer.from([versionByte]), xOnlyPubKey]); - const words = convertBits(payload, 8, 5, true); - - return kaspaEncode(hrp, words); -} - -/** - * Validates a secp256k1 public key (compressed or uncompressed) - */ -export function isValidPublicKey(pub: string): boolean { - if (!pub || typeof pub !== 'string') { - return false; - } - try { - const buf = Buffer.from(pub, 'hex'); - if (buf.length === 33) { - return buf[0] === 0x02 || buf[0] === 0x03; - } - if (buf.length === 65) { - return buf[0] === 0x04; - } - return false; - } catch { - return false; - } -} - -/** - * Validates a secp256k1 private key (32-byte hex) - */ -export function isValidPrivateKey(prv: string): boolean { - if (!prv || typeof prv !== 'string') { - return false; - } - if (isValidXprv(prv) || isValidXpub(prv)) { - return true; - } - try { - const buf = Buffer.from(prv.slice(0, 64), 'hex'); - return buf.length === 32; - } catch { - return false; - } -} - -/** - * Validates a transaction ID (64-char hex) - */ -export function isValidTransactionId(txId: string): boolean { - return /^[0-9a-fA-F]{64}$/.test(txId); -} - -export class Utils implements BaseUtils { - isValidAddress(address: string): boolean { - return isValidKaspaAddress(address); - } - - isValidBlockId(hash: string): boolean { - return /^[0-9a-fA-F]{64}$/.test(hash); - } - - isValidPrivateKey(key: string): boolean { - return isValidPrivateKey(key); - } - - isValidPublicKey(key: string): boolean { - return isValidPublicKey(key); - } - - isValidSignature(signature: string): boolean { - return /^[0-9a-fA-F]{128,130}$/.test(signature); - } - - isValidTransactionId(txId: string): boolean { - return isValidTransactionId(txId); - } -} - -const utils = new Utils(); -export default utils; diff --git a/modules/sdk-coin-kaspa/src/register.ts b/modules/sdk-coin-kaspa/src/register.ts deleted file mode 100644 index 2bdbb8ff51..0000000000 --- a/modules/sdk-coin-kaspa/src/register.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BitGoBase } from '@bitgo/sdk-core'; -import { Kaspa } from './kaspa'; - -export const register = (sdk: BitGoBase): void => { - sdk.register('kaspa', Kaspa.createInstance); - sdk.register('tkaspa', Kaspa.createInstance); -}; diff --git a/modules/sdk-coin-kaspa/test/fixtures/kaspa.fixtures.ts b/modules/sdk-coin-kaspa/test/fixtures/kaspa.fixtures.ts deleted file mode 100644 index afaa678578..0000000000 --- a/modules/sdk-coin-kaspa/test/fixtures/kaspa.fixtures.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Kaspa (KASPA) Test Fixtures - * - * Deterministic test data derived from a fixed private key. - * Do NOT use these keys in production. - */ - -import { KeyPair } from '../../src/lib/keyPair'; -import { compressedToXOnly, buildP2PKScriptPublicKey } from '../../src/lib/sighash'; -import { KaspaTransactionData, KaspaUtxoInput } from '../../src/lib/iface'; - -// Fixed 32-byte private key for deterministic tests only -export const TEST_PRV_KEY = 'b94f5374fce5edbc8e2a8697c15331677e6ebf0b000000000000000000000001'; - -const _kp = new KeyPair({ prv: TEST_PRV_KEY }); -const _keys = _kp.getKeys(); -const _xOnlyPub = compressedToXOnly(Buffer.from(_keys.pub, 'hex')); -const _scriptPublicKey = buildP2PKScriptPublicKey(_xOnlyPub).toString('hex'); - -export const KEYS = { - prv: TEST_PRV_KEY, - pub: _keys.pub, -}; - -export const ADDRESSES = { - valid: _kp.getAddress('mainnet'), - testnet: _kp.getAddress('testnet'), - invalid: 'notanaddress', - sender: _kp.getAddress('mainnet'), - recipient: _kp.getAddress('mainnet'), -}; - -export const SCRIPT_PUBLIC_KEY = _scriptPublicKey; - -// Getters return fresh objects to prevent mutation leakage between tests -export const UTXOS = { - get simple(): KaspaUtxoInput { - return { - transactionId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - transactionIndex: 0, - amount: '100000000', - scriptPublicKey: _scriptPublicKey, - sequence: '0', - sigOpCount: 1, - }; - }, - get second(): KaspaUtxoInput { - return { - transactionId: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', - transactionIndex: 1, - amount: '200000000', - scriptPublicKey: _scriptPublicKey, - sequence: '0', - sigOpCount: 1, - }; - }, -}; - -// Use getter functions to return fresh deep copies, preventing test mutation leakage -export const TRANSACTIONS = { - get simple(): KaspaTransactionData { - return JSON.parse( - JSON.stringify({ - version: 0, - inputs: [UTXOS.simple], - outputs: [ - { - address: _kp.getAddress('mainnet'), - amount: '99998000', - scriptPublicKey: _scriptPublicKey, - }, - ], - fee: '2000', - lockTime: '0', - subnetworkId: '0000000000000000000000000000000000000000', - payload: '', - }) - ) as KaspaTransactionData; - }, - get multiInput(): KaspaTransactionData { - return JSON.parse( - JSON.stringify({ - version: 0, - inputs: [UTXOS.simple, UTXOS.second], - outputs: [ - { - address: _kp.getAddress('mainnet'), - amount: '299998000', - scriptPublicKey: _scriptPublicKey, - }, - ], - fee: '2000', - lockTime: '0', - subnetworkId: '0000000000000000000000000000000000000000', - payload: '', - }) - ) as KaspaTransactionData; - }, -}; diff --git a/modules/sdk-coin-kaspa/test/unit/coin.test.ts b/modules/sdk-coin-kaspa/test/unit/coin.test.ts deleted file mode 100644 index b3de4ea21e..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/coin.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import * as should from 'should'; -import { coins } from '@bitgo/statics'; -import { Kaspa } from '../../src'; -import { KeyPair } from '../../src/lib/keyPair'; -import { TransactionBuilder } from '../../src/lib/transactionBuilder'; -import { Transaction } from '../../src/lib/transaction'; -import { ADDRESSES, KEYS, UTXOS } from '../fixtures/kaspa.fixtures'; - -async function buildSignedTxHex(coinName: string): Promise { - const builder = new TransactionBuilder(coins.get(coinName)); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - tx.sign(Buffer.from(KEYS.prv, 'hex')); - return tx.toHex(); -} - -async function buildUnsignedTxHex(coinName: string): Promise { - const builder = new TransactionBuilder(coins.get(coinName)); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - return tx.toHex(); -} - -describe('Kaspa (KASPA)', function () { - let kaspa: Kaspa; - let tkaspa: Kaspa; - - before(function () { - const mockBitgo = { - url: () => '', - microservicesUrl: () => '', - post: () => ({ result: () => Promise.resolve({}) }), - get: () => ({ result: () => Promise.resolve({}) }), - } as any; - kaspa = new Kaspa(mockBitgo, coins.get('kaspa')); - tkaspa = new Kaspa(mockBitgo, coins.get('tkaspa')); - }); - - describe('Coin Properties', function () { - it('should have the correct chain name', function () { - kaspa.getChain().should.equal('kaspa'); - tkaspa.getChain().should.equal('tkaspa'); - }); - - it('should have the correct family', function () { - kaspa.getFamily().should.equal('kaspa'); - }); - - it('should have the correct full name', function () { - kaspa.getFullName().should.equal('Kaspa'); - tkaspa.getFullName().should.equal('Testnet Kaspa'); - }); - - it('should have the correct base factor (10^8)', function () { - kaspa.getBaseFactor().should.equal(100000000); - }); - - it('should support TSS (ECDSA MPC)', function () { - kaspa.supportsTss().should.be.true(); - kaspa.getMPCAlgorithm().should.equal('ecdsa'); - }); - }); - - describe('Key Validation', function () { - it('should validate a valid public key', function () { - kaspa.isValidPub('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798').should.be.true(); - }); - - it('should reject an invalid public key', function () { - kaspa.isValidPub('not-a-key').should.be.false(); - }); - - it('should validate a valid private key', function () { - kaspa.isValidPrv('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2').should.be.true(); - }); - }); - - describe('Key Generation', function () { - it('should generate a key pair', function () { - const kp = kaspa.generateKeyPair(); - should.exist(kp.pub); - should.exist(kp.prv); - kp.pub!.should.have.length(66); - kp.prv!.should.have.length(64); - }); - - it('should generate a key pair from seed', function () { - const seed = Buffer.alloc(32, 1); - const kp = kaspa.generateKeyPair(seed); - should.exist(kp.pub); - should.exist(kp.prv); - }); - - it('should generate consistent keys from same seed', function () { - const seed = Buffer.alloc(32, 42); - const kp1 = kaspa.generateKeyPair(seed); - const kp2 = kaspa.generateKeyPair(seed); - kp1.pub!.should.equal(kp2.pub!); - kp1.prv!.should.equal(kp2.prv!); - }); - }); - - describe('Address Validation', function () { - it('should validate a mainnet address', function () { - const kp = new KeyPair(); - const address = kp.getAddress('mainnet'); - kaspa.isValidAddress(address).should.be.true(); - }); - - it('should reject an invalid address', function () { - kaspa.isValidAddress('not-an-address').should.be.false(); - }); - - it('should reject empty address', function () { - kaspa.isValidAddress('').should.be.false(); - }); - }); - - describe('isWalletAddress', function () { - it('should return true when address matches derived address from keychains[0]', async function () { - const kp = new KeyPair({ prv: KEYS.prv }); - const address = kp.getAddress('mainnet'); - const keychains = [{ pub: KEYS.pub }, { pub: KEYS.pub }, { pub: KEYS.pub }]; - const result = await kaspa.isWalletAddress({ address, keychains } as any); - result.should.be.true(); - }); - - it('should throw on invalid address', async function () { - await kaspa - .isWalletAddress({ address: 'not-an-address', keychains: [] } as any) - .should.be.rejectedWith(/invalid address/); - }); - - it('should throw when keychains count is not 3', async function () { - const kp = new KeyPair({ prv: KEYS.prv }); - const address = kp.getAddress('mainnet'); - await kaspa - .isWalletAddress({ address, keychains: [{ pub: KEYS.pub }] } as any) - .should.be.rejectedWith(/Invalid keychains/); - }); - - it('should throw when derived address does not match', async function () { - const other = new KeyPair().getKeys(); - const kp = new KeyPair({ prv: KEYS.prv }); - const address = kp.getAddress('mainnet'); - const keychains = [{ pub: other.pub }, { pub: other.pub }, { pub: other.pub }]; - await kaspa.isWalletAddress({ address, keychains } as any).should.be.rejectedWith(/address validation failure/); - }); - }); - - describe('parseTransaction', function () { - it('should return empty object when no txHex is provided', async function () { - const parsed = await kaspa.parseTransaction({}); - parsed.should.deepEqual({}); - }); - - it('should return inputs and outputs for a valid txHex', async function () { - const txHex = await buildUnsignedTxHex('kaspa'); - const parsed = (await kaspa.parseTransaction({ txHex })) as { - inputs: { amount: string; coin: string }[]; - outputs: { address: string; amount: string; coin: string }[]; - }; - parsed.inputs.should.have.length(1); - parsed.inputs[0].amount.should.equal(UTXOS.simple.amount); - parsed.inputs[0].coin.should.equal('kaspa'); - parsed.outputs.should.have.length(1); - parsed.outputs[0].amount.should.equal('99998000'); - parsed.outputs[0].address.should.equal(ADDRESSES.recipient); - parsed.outputs[0].coin.should.equal('kaspa'); - }); - - it('should throw on invalid txHex', async function () { - await kaspa.parseTransaction({ txHex: 'notvalidhex!!' }).should.be.rejectedWith(/Invalid transaction/); - }); - }); - - describe('explainTransaction', function () { - it('should explain a valid transaction', async function () { - const txHex = await buildUnsignedTxHex('kaspa'); - const explained = await kaspa.explainTransaction({ txHex }); - explained.outputs.should.have.length(1); - explained.outputs[0].amount.should.equal('99998000'); - }); - - it('should throw when txHex is missing', async function () { - await kaspa.explainTransaction({} as any).should.be.rejectedWith(/missing transaction hex/); - }); - }); - - describe('signTransaction', function () { - it('should sign a prebuilt transaction', async function () { - const txHex = await buildUnsignedTxHex('kaspa'); - const result = (await kaspa.signTransaction({ - txPrebuild: { txHex }, - prv: KEYS.prv, - } as any)) as { txHex: string }; - result.txHex.should.be.a.String(); - const signed = Transaction.fromHex('kaspa', result.txHex); - signed.signature[0].should.have.length(130); - }); - - it('should throw when txHex is missing', async function () { - await kaspa.signTransaction({ txPrebuild: {}, prv: KEYS.prv } as any).should.be.rejectedWith(/missing txHex/); - }); - }); - - describe('verifyTransaction', function () { - it('should verify a valid transaction', async function () { - const txHex = await buildSignedTxHex('kaspa'); - const result = await kaspa.verifyTransaction({ - txPrebuild: { txHex }, - txParams: { recipients: [{ address: ADDRESSES.recipient, amount: '99998000' }] }, - } as any); - result.should.be.true(); - }); - - it('should throw when txHex is missing', async function () { - await kaspa - .verifyTransaction({ txPrebuild: {}, txParams: {} } as any) - .should.be.rejectedWith(/missing required tx prebuild property txHex/); - }); - }); -}); diff --git a/modules/sdk-coin-kaspa/test/unit/keyPair.test.ts b/modules/sdk-coin-kaspa/test/unit/keyPair.test.ts deleted file mode 100644 index 3515535a3f..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/keyPair.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as should from 'should'; -import { KeyPair } from '../../src/lib/keyPair'; -import { KEYS } from '../fixtures/kaspa.fixtures'; - -describe('Kaspa KeyPair', function () { - describe('Key Generation', function () { - it('should generate a random key pair', function () { - const kp = new KeyPair(); - const keys = kp.getKeys(); - should.exist(keys.pub); - should.exist(keys.prv); - keys.pub.should.have.length(66); // compressed secp256k1 public key - keys.prv!.should.have.length(64); // 32-byte private key in hex - }); - - it('should create a key pair from a private key', function () { - const kp = new KeyPair({ prv: KEYS.prv }); - const keys = kp.getKeys(); - keys.pub.should.equal(KEYS.pub); - keys.prv!.should.equal(KEYS.prv); - }); - - it('should create a key pair from a public key', function () { - const kp = new KeyPair({ pub: KEYS.pub }); - const keys = kp.getKeys(); - keys.pub.should.equal(KEYS.pub); - should.not.exist(keys.prv); - }); - - it('should throw for an invalid private key', function () { - (() => new KeyPair({ prv: 'invalid-prv' })).should.throw(); - }); - }); - - describe('Address Derivation', function () { - it('should derive a mainnet address', function () { - const kp = new KeyPair({ prv: KEYS.prv }); - const address = kp.getAddress('mainnet'); - address.should.startWith('kaspa'); - address.should.containEql(':'); - }); - - it('should derive a testnet address', function () { - const kp = new KeyPair({ prv: KEYS.prv }); - const address = kp.getAddress('testnet'); - address.should.startWith('kaspatest'); - address.should.containEql(':'); - }); - - it('should derive consistent addresses for the same key', function () { - const kp1 = new KeyPair({ prv: KEYS.prv }); - const kp2 = new KeyPair({ prv: KEYS.prv }); - kp1.getAddress('mainnet').should.equal(kp2.getAddress('mainnet')); - }); - - it('should derive different addresses for different keys', function () { - const kp1 = new KeyPair({ prv: KEYS.prv }); - const kp2 = new KeyPair(); - kp1.getAddress('mainnet').should.not.equal(kp2.getAddress('mainnet')); - }); - }); -}); diff --git a/modules/sdk-coin-kaspa/test/unit/transaction.test.ts b/modules/sdk-coin-kaspa/test/unit/transaction.test.ts deleted file mode 100644 index 71f11fe23b..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/transaction.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import assert from 'assert'; -import { Transaction } from '../../src/lib/transaction'; -import { TransactionType } from '@bitgo/sdk-core'; -import { SIGHASH_ALL } from '../../src/lib/sighash'; -import { KEYS, TRANSACTIONS } from '../fixtures/kaspa.fixtures'; - -const COIN = 'kaspa'; - -describe('Kaspa Transaction', function () { - describe('Constructor', function () { - it('should create an empty transaction', function () { - const tx = new Transaction(COIN); - assert.ok(tx); - assert.deepEqual(tx.txData.inputs, []); - assert.deepEqual(tx.txData.outputs, []); - }); - - it('should create a transaction from txData', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - assert.equal(tx.txData.inputs.length, 1); - assert.equal(tx.txData.outputs.length, 1); - assert.equal(tx.txData.fee, '2000'); - }); - }); - - describe('id getter', function () { - it('should return empty string when no id is set', function () { - const tx = new Transaction(COIN); - assert.equal(tx.id, ''); - }); - - it('should return the transaction id if set', function () { - const txData = { ...TRANSACTIONS.simple, id: 'deadbeef' + '00'.repeat(30) }; - const tx = new Transaction(COIN, txData); - assert.equal(tx.id, txData.id); - }); - }); - - describe('signature getter', function () { - it('should return empty signatures for unsigned tx', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const sigs = tx.signature; - assert.equal(sigs.length, TRANSACTIONS.simple.inputs.length); - assert.ok(sigs.every((s) => s === '')); - }); - - it('should return signature scripts after signing', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const sigs = tx.signature; - assert.equal(sigs.length, 1); - assert.ok(sigs[0].length > 0); - }); - }); - - describe('canSign', function () { - it('should always return true', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - assert.ok(tx.canSign({ key: KEYS.prv })); - }); - }); - - describe('sign', function () { - it('should throw on non-32-byte private key', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - assert.throws(() => { - tx.sign(Buffer.from('0102', 'hex')); - }, /32-byte/); - }); - - it('should sign all inputs with a valid private key', function () { - const tx = new Transaction(COIN, { ...TRANSACTIONS.simple }); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - for (const input of tx.txData.inputs) { - assert.ok(input.signatureScript, 'Each input should have a signatureScript'); - assert.ok(input.signatureScript!.length > 0); - } - }); - - it('should sign multiple inputs', function () { - const tx = new Transaction(COIN, TRANSACTIONS.multiInput); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - assert.equal(tx.txData.inputs.length, 2); - for (const input of tx.txData.inputs) { - assert.ok(input.signatureScript && input.signatureScript.length > 0); - } - }); - - it('should produce 65-byte signatures (64 Schnorr + 1 sighash type)', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const sigHex = tx.txData.inputs[0].signatureScript!; - // 65 bytes = 130 hex chars - assert.equal(sigHex.length, 130); - // Last byte is sighash type (0x01 = SIGHASH_ALL) - const lastByte = parseInt(sigHex.slice(-2), 16); - assert.equal(lastByte, SIGHASH_ALL); - }); - }); - - describe('verifySignature', function () { - it('should return false for unsigned input', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const pubKey = Buffer.from(KEYS.pub, 'hex'); - assert.equal(tx.verifySignature(pubKey, 0), false); - }); - - it('should return true after signing with matching key', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const pubKey = Buffer.from(KEYS.pub, 'hex'); - assert.ok(tx.verifySignature(pubKey, 0)); - }); - - it('should accept x-only (32-byte) public key', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const xOnlyPub = Buffer.from(KEYS.pub, 'hex').slice(1); - assert.ok(tx.verifySignature(xOnlyPub, 0)); - }); - - it('should return false for wrong public key', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const wrongPub = Buffer.from('02' + 'ab'.repeat(32), 'hex'); - assert.equal(tx.verifySignature(wrongPub, 0), false); - }); - - it('should return false for out-of-range input index', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - const pubKey = Buffer.from(KEYS.pub, 'hex'); - assert.equal(tx.verifySignature(pubKey, 99), false); - }); - }); - - describe('explainTransaction', function () { - it('should return a TransactionExplanation with correct fields', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const explanation = tx.explainTransaction(); - - assert.equal(explanation.id, ''); - assert.equal(explanation.type, TransactionType.Send); - assert.equal(explanation.outputs.length, 1); - assert.equal(explanation.outputs[0].amount, '99998000'); - assert.equal(explanation.inputs.length, 1); - assert.equal(explanation.inputs[0].amount, '100000000'); - }); - - it('should calculate the fee as totalIn - totalOut', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const explanation = tx.explainTransaction(); - // fee = 100000000 - 99998000 = 2000 - assert.equal(explanation.fee.fee, '2000'); - }); - - it('should sum all outputs in outputAmount', function () { - const tx = new Transaction(COIN, TRANSACTIONS.multiInput); - const explanation = tx.explainTransaction(); - assert.equal(explanation.outputAmount, '299998000'); - }); - }); - - describe('Serialization', function () { - it('toJson should return a copy of txData', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const json = tx.toJson(); - assert.deepEqual(json, TRANSACTIONS.simple); - assert.notEqual(json, tx.txData); - }); - - it('toBroadcastFormat should return a JSON string', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const broadcast = tx.toBroadcastFormat(); - assert.equal(typeof broadcast, 'string'); - const parsed = JSON.parse(broadcast); - assert.equal(parsed.version, 0); - assert.equal(parsed.inputs.length, 1); - }); - - it('toHex should return hex-encoded JSON', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const hex = tx.toHex(); - assert.ok(/^[0-9a-fA-F]+$/.test(hex)); - const decoded = Buffer.from(hex, 'hex').toString(); - const parsed = JSON.parse(decoded); - assert.equal(parsed.version, 0); - }); - - it('fromHex should reconstruct the transaction', function () { - const original = new Transaction(COIN, TRANSACTIONS.simple); - const hex = original.toHex(); - const restored = Transaction.fromHex(COIN, hex); - assert.deepEqual(restored.toJson(), original.toJson()); - }); - - it('fromJson should reconstruct from object', function () { - const original = new Transaction(COIN, TRANSACTIONS.simple); - const json = original.toJson(); - const restored = Transaction.fromJson(COIN, json); - assert.deepEqual(restored.toJson(), json); - }); - - it('fromJson should reconstruct from string', function () { - const original = new Transaction(COIN, TRANSACTIONS.simple); - const jsonStr = JSON.stringify(original.toJson()); - const restored = Transaction.fromJson(COIN, jsonStr); - assert.deepEqual(restored.toJson(), original.toJson()); - }); - - it('round-trip through toHex/fromHex should preserve signatures', function () { - const tx = new Transaction(COIN, TRANSACTIONS.simple); - const privKey = Buffer.from(KEYS.prv, 'hex'); - tx.sign(privKey); - - const hex = tx.toHex(); - const restored = Transaction.fromHex(COIN, hex); - - assert.deepEqual(restored.signature, tx.signature); - }); - }); -}); diff --git a/modules/sdk-coin-kaspa/test/unit/transactionBuilder.test.ts b/modules/sdk-coin-kaspa/test/unit/transactionBuilder.test.ts deleted file mode 100644 index 62baf71f47..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/transactionBuilder.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import assert from 'assert'; -import { coins } from '@bitgo/statics'; -import { Transaction } from '../../src/lib/transaction'; -import { TransactionBuilder } from '../../src/lib/transactionBuilder'; -import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; -import { DEFAULT_FEE } from '../../src/lib/constants'; -import { ADDRESSES, KEYS, UTXOS } from '../fixtures/kaspa.fixtures'; - -const coinConfig = coins.get('kaspa'); - -describe('Kaspa TransactionBuilder', function () { - let builder: TransactionBuilder; - - beforeEach(function () { - builder = new TransactionBuilder(coinConfig); - }); - - describe('sender', function () { - it('should set the sender address and return this (fluent)', function () { - const result = builder.sender(ADDRESSES.sender); - assert.equal(result, builder); - }); - - it('should throw on invalid sender address', function () { - assert.throws(() => { - builder.sender(ADDRESSES.invalid); - }, /Invalid Kaspa address/); - }); - }); - - describe('to', function () { - it('should add an output and return this (fluent)', function () { - const result = builder.to(ADDRESSES.recipient, '1000'); - assert.equal(result, builder); - }); - - it('should throw on invalid recipient address', function () { - assert.throws(() => { - builder.to('notanaddress', '1000'); - }, /Invalid Kaspa/); - }); - }); - - describe('fee', function () { - it('should set the fee and return this', function () { - const result = builder.fee('5000'); - assert.equal(result, builder); - }); - - it('should default to DEFAULT_FEE', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000'); - const tx = (await builder.build()) as Transaction; - assert.equal(tx.txData.fee, DEFAULT_FEE); - }); - }); - - describe('addInput', function () { - it('should add a UTXO input and return this', function () { - const result = builder.addInput(UTXOS.simple); - assert.equal(result, builder); - }); - - it('should throw when transactionId is missing', function () { - assert.throws(() => { - builder.addInput({ ...UTXOS.simple, transactionId: '' }); - }, /Invalid UTXO/); - }); - - it('should throw when transactionIndex is undefined', function () { - assert.throws(() => { - builder.addInput({ ...UTXOS.simple, transactionIndex: undefined as unknown as number }); - }, /Invalid UTXO/); - }); - }); - - describe('addInputs', function () { - it('should add multiple inputs', function () { - builder.addInputs([UTXOS.simple, UTXOS.second]); - }); - }); - - describe('validateTransaction', function () { - it('should throw when no inputs are set', function () { - builder.to(ADDRESSES.recipient, '1000'); - assert.throws(() => { - builder.validateTransaction(); - }, /At least one UTXO input/); - }); - - it('should throw when no outputs are set', function () { - builder.addInput(UTXOS.simple); - assert.throws(() => { - builder.validateTransaction(); - }, /At least one output/); - }); - - it('should not throw with valid inputs and outputs', function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000'); - assert.doesNotThrow(() => { - builder.validateTransaction(); - }); - }); - }); - - describe('validateAddress', function () { - it('should not throw for a valid address', function () { - assert.doesNotThrow(() => { - builder.validateAddress({ address: ADDRESSES.valid }); - }); - }); - - it('should throw for an invalid address', function () { - assert.throws(() => { - builder.validateAddress({ address: 'invalid' }); - }, /Invalid Kaspa address/); - }); - }); - - describe('validateRawTransaction', function () { - it('should not throw for valid hex', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000'); - const tx = (await builder.build()) as Transaction; - const hex = tx.toHex(); - assert.doesNotThrow(() => { - builder.validateRawTransaction(hex); - }); - }); - - it('should throw for invalid hex', function () { - assert.throws(() => { - builder.validateRawTransaction('notvalidhex!!'); - }, /Invalid raw Kaspa/); - }); - }); - - describe('validateValue', function () { - it('should not throw for non-negative value', function () { - const BigNumber = require('bignumber.js').default ?? require('bignumber.js'); - assert.doesNotThrow(() => { - builder.validateValue(new BigNumber(0)); - }); - assert.doesNotThrow(() => { - builder.validateValue(new BigNumber(1000000)); - }); - }); - - it('should throw for negative value', function () { - const BigNumber = require('bignumber.js').default ?? require('bignumber.js'); - assert.throws(() => { - builder.validateValue(new BigNumber(-1)); - }, /negative/); - }); - }); - - describe('build', function () { - it('should build a valid transaction from inputs + outputs', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - - assert.ok(tx instanceof Transaction); - assert.equal(tx.txData.version, 0); - assert.equal(tx.txData.inputs.length, 1); - assert.equal(tx.txData.outputs.length, 1); - assert.equal(tx.txData.outputs[0].amount, '99998000'); - assert.equal(tx.txData.fee, '2000'); - }); - - it('should build a multi-input transaction', async function () { - builder.addInputs([UTXOS.simple, UTXOS.second]).to(ADDRESSES.recipient, '299998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - - assert.equal(tx.txData.inputs.length, 2); - assert.equal(tx.txData.outputs.length, 1); - }); - - it('should include all standard fields', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000'); - const tx = (await builder.build()) as Transaction; - - assert.equal(tx.txData.version, 0); - assert.equal(tx.txData.lockTime, '0'); - assert.equal(tx.txData.subnetworkId, '0000000000000000000000000000000000000000'); - }); - }); - - describe('sign', function () { - it('should sign the built transaction via signImplementation', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - builder.sign({ key: KEYS.prv }); - assert.ok(tx.signature[0].length > 0, 'input should be signed'); - assert.equal(tx.signature[0].length, 130, 'Schnorr sig should be 65 bytes / 130 hex chars'); - }); - - it('should sign all inputs of a multi-input transaction', async function () { - builder.addInputs([UTXOS.simple, UTXOS.second]).to(ADDRESSES.recipient, '299998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - builder.sign({ key: KEYS.prv }); - assert.equal(tx.signature.length, 2); - assert.ok(tx.signature.every((s) => s.length === 130)); - }); - - it('should throw SigningError when private key is missing', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000'); - await builder.build(); - assert.throws(() => { - builder.sign({ key: '' }); - }); - }); - }); - - describe('from (rebuild from hex)', function () { - it('should reconstruct a builder from a serialized transaction', async function () { - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - const hex = tx.toHex(); - - const newBuilder = new TransactionBuilder(coinConfig); - newBuilder.from(hex); - const rebuilt = (await newBuilder.build()) as Transaction; - - assert.deepEqual(rebuilt.toJson(), tx.toJson()); - }); - }); -}); - -describe('Kaspa TransactionBuilderFactory', function () { - let factory: TransactionBuilderFactory; - - beforeEach(function () { - factory = new TransactionBuilderFactory(coinConfig); - }); - - describe('getBuilder', function () { - it('should return a new TransactionBuilder', function () { - const builder = factory.getBuilder(); - assert.ok(builder instanceof TransactionBuilder); - }); - }); - - describe('from', function () { - it('should reconstruct a builder from a serialized transaction hex', async function () { - const originalBuilder = factory.getBuilder(); - originalBuilder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await originalBuilder.build()) as Transaction; - const hex = tx.toHex(); - - const rebuiltBuilder = factory.from(hex); - const rebuiltTx = (await rebuiltBuilder.build()) as Transaction; - - assert.deepEqual(rebuiltTx.toJson(), tx.toJson()); - }); - }); -}); diff --git a/modules/sdk-coin-kaspa/test/unit/transactionFlow.test.ts b/modules/sdk-coin-kaspa/test/unit/transactionFlow.test.ts deleted file mode 100644 index 94afc2120d..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/transactionFlow.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Kaspa End-to-End Transaction Flow - * - * Covers: build → sign → serialize → deserialize → verify - * All operations are offline (no live RPC calls). - */ - -import assert from 'assert'; -import { coins } from '@bitgo/statics'; -import { TransactionBuilder } from '../../src/lib/transactionBuilder'; -import { Transaction } from '../../src/lib/transaction'; -import { KEYS, ADDRESSES, UTXOS } from '../fixtures/kaspa.fixtures'; - -const coinConfig = coins.get('kaspa'); -const PRV_KEY_BUF = Buffer.from(KEYS.prv, 'hex'); - -describe('Kaspa — End-to-End Transaction Flow', function () { - it('should build, sign, serialize, deserialize, and verify a simple transaction', async function () { - // Step 1: Build - const builder = new TransactionBuilder(coinConfig); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - - const tx = (await builder.build()) as Transaction; - assert.ok(tx instanceof Transaction, 'build() should return a Transaction'); - assert.equal(tx.txData.inputs.length, 1, 'should have 1 input'); - assert.equal(tx.txData.outputs.length, 1, 'should have 1 output'); - assert.equal(tx.txData.outputs[0].amount, '99998000'); - - // Step 2: Sign - tx.sign(PRV_KEY_BUF); - const sigs = tx.signature; - assert.ok(sigs.length > 0, 'should have signatures after signing'); - assert.ok(sigs[0].length > 0, 'signature should be non-empty'); - - // Step 3: Verify signature with public key - const pubKey = Buffer.from(KEYS.pub, 'hex'); - assert.ok(tx.verifySignature(pubKey, 0), 'signature should verify against correct pubkey'); - - // Step 4: Serialize to broadcast format - const broadcastPayload = tx.toBroadcastFormat(); - assert.ok(broadcastPayload, 'toBroadcastFormat should return a non-empty string'); - assert.doesNotThrow(() => JSON.parse(broadcastPayload), 'broadcast format should be valid JSON'); - - // Step 5: Serialize to hex - const hex = tx.toHex(); - assert.ok(/^[0-9a-fA-F]+$/.test(hex), 'toHex should return valid hex'); - - // Step 6: Deserialize and verify round-trip - const reloaded = Transaction.fromHex(coinConfig.name, hex); - assert.deepEqual(reloaded.txData.inputs, tx.txData.inputs); - assert.deepEqual(reloaded.txData.outputs, tx.txData.outputs); - assert.equal(reloaded.txData.fee, tx.txData.fee); - - // Step 7: Explain the transaction - const explanation = tx.explainTransaction(); - assert.ok(explanation.outputs.length > 0, 'explanation should have outputs'); - assert.equal(explanation.outputs[0].amount, '99998000'); - assert.ok(explanation.fee, 'explanation should have fee'); - }); - - it('should NOT be considered fully signed when unsigned', async function () { - const builder = new TransactionBuilder(coinConfig); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - - const tx = (await builder.build()) as Transaction; - const sigs = tx.signature; - - assert.ok( - sigs.every((s) => s === ''), - 'unsigned transaction should have empty signature scripts' - ); - - const payload = tx.toBroadcastFormat(); - const parsed = JSON.parse(payload); - assert.ok(!parsed.inputs[0].signatureScript, 'inputs should not have signatureScript when unsigned'); - }); - - it('should sign multiple inputs independently', async function () { - const builder = new TransactionBuilder(coinConfig); - builder.addInputs([UTXOS.simple, UTXOS.second]).to(ADDRESSES.recipient, '299998000').fee('2000'); - - const tx = (await builder.build()) as Transaction; - tx.sign(PRV_KEY_BUF); - - const pubKey = Buffer.from(KEYS.pub, 'hex'); - assert.equal(tx.txData.inputs.length, 2); - assert.ok(tx.verifySignature(pubKey, 0), 'first input signature should verify'); - assert.ok(tx.verifySignature(pubKey, 1), 'second input signature should verify'); - }); - - it('should rebuild from serialized hex and produce identical broadcast format', async function () { - const builder = new TransactionBuilder(coinConfig); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - tx.sign(PRV_KEY_BUF); - - const originalHex = tx.toHex(); - const originalPayload = tx.toBroadcastFormat(); - - const rebuilt = Transaction.fromHex(coinConfig.name, originalHex); - const rebuiltPayload = rebuilt.toBroadcastFormat(); - - assert.equal(rebuiltPayload, originalPayload, 'serialization should be deterministic'); - }); - - it('should produce a valid RPC-submittable JSON broadcast payload', async function () { - const builder = new TransactionBuilder(coinConfig); - builder.addInput(UTXOS.simple).to(ADDRESSES.recipient, '99998000').fee('2000'); - const tx = (await builder.build()) as Transaction; - tx.sign(PRV_KEY_BUF); - - const payload = tx.toBroadcastFormat(); - - assert.ok(payload.length > 0, 'payload must be non-empty'); - const parsed = JSON.parse(payload); - - assert.ok(parsed.version !== undefined, 'must have version'); - assert.ok(Array.isArray(parsed.inputs), 'must have inputs array'); - assert.ok(Array.isArray(parsed.outputs), 'must have outputs array'); - assert.ok(parsed.inputs.length > 0, 'must have at least one input'); - assert.ok(parsed.outputs.length > 0, 'must have at least one output'); - - for (const input of parsed.inputs) { - assert.ok(input.signatureScript, 'signed input must have signatureScript'); - assert.equal(input.signatureScript.length, 130, 'Schnorr sig should be 65 bytes = 130 hex chars'); - } - }); -}); diff --git a/modules/sdk-coin-kaspa/test/unit/utils.test.ts b/modules/sdk-coin-kaspa/test/unit/utils.test.ts deleted file mode 100644 index cf05d2f2a2..0000000000 --- a/modules/sdk-coin-kaspa/test/unit/utils.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import assert from 'assert'; -import { - isValidKaspaAddress, - isValidMainnetAddress, - isValidTestnetAddress, - isValidPublicKey, - isValidPrivateKey, - isValidTransactionId, - pubKeyToKaspaAddress, - Utils, -} from '../../src/lib/utils'; -import { KeyPair } from '../../src/lib/keyPair'; -import { KEYS, ADDRESSES, UTXOS } from '../fixtures/kaspa.fixtures'; - -describe('Kaspa Utils', function () { - describe('isValidKaspaAddress', function () { - it('should accept a valid mainnet address', function () { - assert.ok(isValidKaspaAddress(ADDRESSES.valid)); - }); - - it('should accept a valid testnet address', function () { - assert.ok(isValidKaspaAddress(ADDRESSES.testnet)); - }); - - it('should reject an empty string', function () { - assert.equal(isValidKaspaAddress(''), false); - }); - - it('should reject a non-kaspa address format', function () { - assert.equal(isValidKaspaAddress('bitcoin:qp9dksrqz9'), false); - }); - - it('should reject a plain invalid string', function () { - assert.equal(isValidKaspaAddress('notanaddress'), false); - }); - - it('should reject null-like input', function () { - assert.equal(isValidKaspaAddress(null as unknown as string), false); - }); - }); - - describe('isValidMainnetAddress', function () { - it('should accept a valid mainnet address', function () { - assert.ok(isValidMainnetAddress(ADDRESSES.valid)); - }); - - it('should reject a testnet address', function () { - assert.equal(isValidMainnetAddress(ADDRESSES.testnet), false); - }); - - it('should reject an empty string', function () { - assert.equal(isValidMainnetAddress(''), false); - }); - }); - - describe('isValidTestnetAddress', function () { - it('should accept a valid testnet address', function () { - assert.ok(isValidTestnetAddress(ADDRESSES.testnet)); - }); - - it('should reject a mainnet address', function () { - assert.equal(isValidTestnetAddress(ADDRESSES.valid), false); - }); - - it('should reject an empty string', function () { - assert.equal(isValidTestnetAddress(''), false); - }); - }); - - describe('pubKeyToKaspaAddress', function () { - it('should derive a valid mainnet address from compressed public key', function () { - const pubKeyBuffer = Buffer.from(KEYS.pub, 'hex'); - const address = pubKeyToKaspaAddress(pubKeyBuffer, 'kaspa'); - assert.ok(address.startsWith('kaspa:')); - assert.ok(isValidKaspaAddress(address)); - }); - - it('should derive a valid testnet address from compressed public key', function () { - const pubKeyBuffer = Buffer.from(KEYS.pub, 'hex'); - const address = pubKeyToKaspaAddress(pubKeyBuffer, 'kaspatest'); - assert.ok(address.startsWith('kaspatest:')); - assert.ok(isValidKaspaAddress(address)); - }); - - it('should accept hex string public key', function () { - const address = pubKeyToKaspaAddress(KEYS.pub, 'kaspa'); - assert.ok(address.startsWith('kaspa:')); - }); - - it('should throw for non-33-byte public key', function () { - assert.throws(() => { - pubKeyToKaspaAddress(Buffer.from('0102030405', 'hex'), 'kaspa'); - }); - }); - - it('should produce same address as KeyPair.getAddress', function () { - const kp = new KeyPair({ pub: KEYS.pub }); - const fromKp = kp.getAddress('mainnet'); - const fromUtil = pubKeyToKaspaAddress(Buffer.from(KEYS.pub, 'hex'), 'kaspa'); - assert.equal(fromUtil, fromKp); - }); - }); - - describe('isValidPublicKey', function () { - it('should accept a valid compressed 33-byte public key', function () { - assert.ok(isValidPublicKey(KEYS.pub)); - }); - - it('should reject an empty string', function () { - assert.equal(isValidPublicKey(''), false); - }); - - it('should reject a string that is not hex', function () { - assert.equal(isValidPublicKey('not_a_key'), false); - }); - - it('should reject incorrect key length', function () { - assert.equal(isValidPublicKey('0102030405'), false); - }); - - it('should accept uncompressed 65-byte public key', function () { - const fakeUncompressed = '04' + 'ab'.repeat(64); - assert.ok(isValidPublicKey(fakeUncompressed)); - }); - - it('should reject a null-like value', function () { - assert.equal(isValidPublicKey(null as unknown as string), false); - }); - }); - - describe('isValidPrivateKey', function () { - it('should accept a valid 32-byte hex private key', function () { - assert.ok(isValidPrivateKey(KEYS.prv)); - }); - - it('should reject an empty string', function () { - assert.equal(isValidPrivateKey(''), false); - }); - - it('should reject a non-hex string', function () { - assert.equal(isValidPrivateKey('not_a_key'), false); - }); - - it('should reject null-like value', function () { - assert.equal(isValidPrivateKey(null as unknown as string), false); - }); - }); - - describe('isValidTransactionId', function () { - it('should accept a valid 64-char hex transaction ID', function () { - assert.ok(isValidTransactionId(UTXOS.simple.transactionId)); - }); - - it('should reject short hex strings', function () { - assert.equal(isValidTransactionId('abcdef1234'), false); - }); - - it('should reject non-hex strings', function () { - assert.equal(isValidTransactionId('xyz' + 'a'.repeat(61)), false); - }); - - it('should reject empty string', function () { - assert.equal(isValidTransactionId(''), false); - }); - }); - - describe('Utils class', function () { - const utils = new Utils(); - - it('isValidAddress should delegate to isValidKaspaAddress', function () { - assert.ok(utils.isValidAddress(ADDRESSES.valid)); - assert.equal(utils.isValidAddress('invalid'), false); - }); - - it('isValidBlockId should accept 64-char hex', function () { - assert.ok(utils.isValidBlockId(UTXOS.simple.transactionId)); - assert.equal(utils.isValidBlockId('short'), false); - }); - - it('isValidPrivateKey should accept valid key', function () { - assert.ok(utils.isValidPrivateKey(KEYS.prv)); - }); - - it('isValidPublicKey should accept valid key', function () { - assert.ok(utils.isValidPublicKey(KEYS.pub)); - }); - - it('isValidSignature should accept 128-char hex', function () { - const sig = 'ab'.repeat(64); - assert.ok(utils.isValidSignature(sig)); - }); - - it('isValidSignature should reject short strings', function () { - assert.equal(utils.isValidSignature('abcd'), false); - }); - - it('isValidTransactionId should accept 64-char hex', function () { - assert.ok(utils.isValidTransactionId(UTXOS.simple.transactionId)); - }); - }); -}); diff --git a/modules/sdk-coin-kaspa/tsconfig.json b/modules/sdk-coin-kaspa/tsconfig.json deleted file mode 100644 index 64aeea134a..0000000000 --- a/modules/sdk-coin-kaspa/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./", - "strictPropertyInitialization": false, - "esModuleInterop": true, - "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] - }, - "include": ["src/**/*", "test/**/*"], - "exclude": ["node_modules"], - "references": [ - { - "path": "../sdk-api" - }, - { - "path": "../sdk-core" - }, - { - "path": "../statics" - }, - { - "path": "../sdk-test" - } - ] -}