Skip to content

feat(sdk-coin-kas): add Kaspa (KAS) unique chain SDK integration#8421

Draft
manojkumar138 wants to merge 2 commits intomasterfrom
feat/kaspa-sdk
Draft

feat(sdk-coin-kas): add Kaspa (KAS) unique chain SDK integration#8421
manojkumar138 wants to merge 2 commits intomasterfrom
feat/kaspa-sdk

Conversation

@manojkumar138
Copy link
Copy Markdown
Contributor

No description provided.

@manojkumar138 manojkumar138 requested review from a team as code owners April 7, 2026 12:27
@sachushaji
Copy link
Copy Markdown
Contributor

@claude

@mohd-kashif mohd-kashif marked this pull request as draft April 8, 2026 05:20
@manojkumar138 manojkumar138 force-pushed the feat/kaspa-sdk branch 2 times, most recently from 00809c6 to 4e4bc10 Compare April 8, 2026 07:32
@mohd-kashif mohd-kashif marked this pull request as ready for review April 9, 2026 07:17
@mohd-kashif mohd-kashif requested a review from Copilot April 9, 2026 07:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-party Kaspa (KAS) support across BitGoJS by introducing a new @bitgo/sdk-coin-kas package and wiring it into statics, BitGo coin registration, account-lib builders, and build/deploy plumbing.

Changes:

  • Added Kaspa statics (coin + networks) and registered kas / tkas in the global coin/token lists.
  • Introduced @bitgo/sdk-coin-kas with keypair, address utilities, transaction model, and builder (+ unit tests).
  • Integrated the new coin into BitGo/account-lib exports & registration, Docker build/link flow, and CODEOWNERS; renamed the existing ERC20 “kas” token to ekas to avoid symbol collision.

Reviewed changes

Copilot reviewed 41 out of 41 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tsconfig.packages.json Adds sdk-coin-kas TS project reference.
modules/statics/test/unit/fixtures/expectedColdFeatures.ts Adds kas / tkas to expected cold features fixture.
modules/statics/src/networks.ts Defines Kaspa network types and registers main/test networks.
modules/statics/src/kas.ts Adds Kaspa statics coin factory/class.
modules/statics/src/index.ts Exports KASCoin from statics.
modules/statics/src/coins/erc20Coins.ts Renames ERC20 “kas” token to ekas.
modules/statics/src/base.ts Adds CoinFamily.KAS and BaseUnit.KAS.
modules/statics/src/allCoinsAndTokens.ts Registers kas and tkas in the global coin list.
modules/sdk-core/src/bitgo/environments.ts Adds kasNodeUrl environment config and default URLs.
modules/sdk-coin-kas/tsconfig.json New TS config for @bitgo/sdk-coin-kas.
modules/sdk-coin-kas/test/unit/utils.test.ts Unit tests for Kaspa address/key utilities.
modules/sdk-coin-kas/test/unit/transactionFlow.test.ts E2E flow test for build/sign/serialize/deserialize.
modules/sdk-coin-kas/test/unit/transactionBuilder.test.ts Unit tests for transaction builder behavior.
modules/sdk-coin-kas/test/unit/transaction.test.ts Unit tests for tx serialization, txid, sighash, explain.
modules/sdk-coin-kas/test/unit/keyPair.test.ts Unit tests for keypair generation/validation/address derivation.
modules/sdk-coin-kas/test/unit/coin.test.ts Unit tests for coin class surface behavior.
modules/sdk-coin-kas/test/fixtures/kas.fixtures.ts Test vectors/fixtures for Kaspa tests.
modules/sdk-coin-kas/src/tkas.ts Adds testnet coin class.
modules/sdk-coin-kas/src/register.ts Adds SDK registration helper for kas / tkas.
modules/sdk-coin-kas/src/lib/utils.ts Implements address encoding/decoding + serialization utils.
modules/sdk-coin-kas/src/lib/transactionBuilderFactory.ts Adds builder factory for transfers/from-raw.
modules/sdk-coin-kas/src/lib/transactionBuilder.ts Implements Kaspa UTXO transaction builder + signing.
modules/sdk-coin-kas/src/lib/transaction.ts Implements tx model, serialization, txid, sighash.
modules/sdk-coin-kas/src/lib/keyPair.ts Implements secp256k1 keypair and address derivation.
modules/sdk-coin-kas/src/lib/index.ts Re-exports coin library primitives.
modules/sdk-coin-kas/src/lib/iface.ts Adds Kaspa type definitions for tx/build/sign flows.
modules/sdk-coin-kas/src/lib/constants.ts Adds Kaspa constants (HRPs, opcodes, fees, URLs, tags).
modules/sdk-coin-kas/src/kas.ts Implements mainnet coin class (sign/explain/verify/etc).
modules/sdk-coin-kas/src/index.ts Package entrypoint exports.
modules/sdk-coin-kas/package.json New package manifest and dependencies.
modules/sdk-coin-kas/.mocharc.yml Mocha config for running TS tests.
modules/sdk-coin-kas/.eslintignore ESLint ignore list for the new module.
modules/bitgo/tsconfig.json Adds sdk-coin-kas TS project reference.
modules/bitgo/src/v2/coins/index.ts Exports Kaspa coin classes from BitGo v2 coins index.
modules/bitgo/src/v2/coinFactory.ts Registers kas / tkas constructors in coin factory.
modules/bitgo/package.json Adds @bitgo/sdk-coin-kas dependency.
modules/account-lib/tsconfig.json Adds sdk-coin-kas TS project reference.
modules/account-lib/src/index.ts Exposes Kaspa builders and adds to builder map.
modules/account-lib/package.json Adds @bitgo/sdk-coin-kas dependency.
Dockerfile Adds build/link steps for sdk-coin-kas in image.
CODEOWNERS Adds code ownership for modules/sdk-coin-kas/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread modules/sdk-coin-kas/src/lib/utils.ts Outdated
Comment on lines +60 to +71
const maxv = (1 << toBits) - 1;
for (const value of data) {
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}
if (pad && bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kaspaConvertBits uses bitwise shifts (<<, >>, |, &) on a JS number, which truncates to 32-bit signed integers. Since Kaspa address payloads are 32 bytes, the accumulator will overflow/wrap during encoding/decoding, producing non-standard addresses/checksums. Rework kaspaConvertBits to avoid 32-bit bitwise ops (e.g., use BigInt or arithmetic + masking like the standard convertbits implementation) and validate leftover bits when pad is false.

Suggested change
const maxv = (1 << toBits) - 1;
for (const value of data) {
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}
if (pad && bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
const fromBase = 2 ** fromBits;
const toBase = 2 ** toBits;
const maxv = toBase - 1;
for (const value of data) {
if (!Number.isInteger(value) || value < 0 || value >= fromBase) {
throw new Error('Invalid value for kaspaConvertBits');
}
acc = acc * fromBase + value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
const divisor = 2 ** bits;
result.push(Math.floor(acc / divisor) % toBase);
acc = acc % divisor;
}
}
if (pad) {
if (bits > 0) {
result.push((acc * (2 ** (toBits - bits))) % toBase);
}
} else if (bits >= fromBits || acc * (2 ** (toBits - bits)) > maxv) {
throw new Error('Invalid incomplete group in kaspaConvertBits');
}

Copilot uses AI. Check for mistakes.
Comment thread modules/sdk-coin-kas/src/lib/utils.ts Outdated
* Kaspa (KAS) Utility Functions
*
* Address validation, encoding, script generation, and other utilities.
* Kaspa uses bech32m-encoded addresses with x-only secp256k1 public keys.
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File header says Kaspa uses “bech32m-encoded addresses”, but this implementation is cashaddr-style with a hrp: separator (and later comments also say “not standard bech32/bech32m”). Please update the top-level documentation to match the actual encoding to avoid misleading integrators.

Suggested change
* Kaspa uses bech32m-encoded addresses with x-only secp256k1 public keys.
* Kaspa uses cashaddr-style addresses with an HRP and ':' separator, and
* x-only secp256k1 public keys.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +51
/**
* Extract x-only public key from address (for script building).
* Kaspa P2PK address payload IS the x-only public key.
*/
function addressToXOnlyPubKey(address: string): Buffer {
const { payload } = kaspaDecodeAddress(address);
return payload;
}

/**
* Build a P2PK script public key from an address.
*/
function addressToScriptPublicKey(address: string): KaspaScriptPublicKey {
const xOnlyPubKey = addressToXOnlyPubKey(address);
return buildScriptPublicKey(xOnlyPubKey);
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressToScriptPublicKey always builds a P2PK locking script from the decoded payload, but isValidAddress allows both pubkey (0x00) and scripthash (0x08) address versions. For a scripthash address this would generate an invalid script and could burn funds. Either restrict builder outputs to pubkey addresses or branch on the decoded version and build the correct script for each address type.

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +196
const sig = secp256k1.sign(sighash, privKeyBytes, { lowS: false });
const sigBytes = Buffer.from(sig.toCompactRawBytes());
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code comments/state indicate Schnorr signatures, but the implementation uses secp256k1.sign(...) which is ECDSA in @noble/curves. Kaspa requires Schnorr signatures; use secp256k1.schnorr.sign(...) (or the correct Kaspa signing primitive) and ensure the produced signature format matches Kaspa’s script expectations.

Suggested change
const sig = secp256k1.sign(sighash, privKeyBytes, { lowS: false });
const sigBytes = Buffer.from(sig.toCompactRawBytes());
const sigBytes = Buffer.from(secp256k1.schnorr.sign(sighash, privKeyBytes));

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +178
async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
const kasParams = params as KaspaSignTransactionOptions;
const txHex = kasParams.txPrebuild?.txHex ?? kasParams.txPrebuild?.halfSigned?.txHex;
if (!txHex) {
throw new SigningError('Missing txHex in transaction prebuild');
}
if (!kasParams.prv) {
throw new SigningError('Missing private key');
}

const txBuilder = this.getBuilder().from(txHex);
txBuilder.sign({ key: kasParams.prv });
const tx = await txBuilder.build();

const signed = tx.toBroadcastFormat();
// Return halfSigned if only one signature (multisig), fully signed if complete
return tx.signature.length >= 2 ? { txHex: signed } : { halfSigned: { txHex: signed } };
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signTransaction reconstructs the builder from txHex and immediately signs, but signing requires per-input UTXO data (utxoEntries) and that data is not serialized into txHex (and Transaction.deserialize doesn’t populate it). As written, txBuilder.sign() will throw for typical prebuilds. Update the prebuild/params to carry the input UTXOs (similar to other UTXO coins’ txPrebuild.txInfo.unspents) and inject them into the builder/transaction before computing sighashes.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +17
"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"
},
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bitgo/sdk-coin-kas’s tests rely on mocha+tsx (.mocharc.yml requires tsx) and other modules typically declare @bitgo/sdk-api / @bitgo/sdk-test (and sometimes tsx) in devDependencies. This package currently has an empty devDependencies, which can break standalone installs/tests/publishing. Align package.json devDependencies with the repo’s other sdk-coin-* packages.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +52
describe('getAddress', () => {
it('should derive a valid mainnet address', () => {
const kp = new KeyPair({ prv: validPrivateKey });
const address = kp.getAddress('mainnet');
address.should.startWith('kaspa:');
isValidAddress(address).should.be.true();
});

it('should derive a valid testnet address', () => {
const kp = new KeyPair({ prv: validPrivateKey });
const address = kp.getAddress('testnet');
address.should.startWith('kaspatest:');
isValidAddress(address).should.be.true();
});
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current KeyPair/address tests only assert prefix + isValidAddress(), which can pass even if the implementation is self-consistent but not spec-compliant. Add an assertion against known-good vectors (e.g., TEST_ACCOUNT.publicKeyTEST_ACCOUNT.mainnetAddress) to catch encoding/signing regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +1977 to +1984
/**
* Kaspa network interface.
* Kaspa is a UTXO-based BlockDAG using GHOSTDAG/Proof-of-Work with bech32m addresses.
*/
export interface KaspaNetwork extends BaseNetwork {
/** bech32m Human-Readable Part ('kaspa' for mainnet, 'kaspatest' for testnet) */
readonly hrp: string;
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kaspa network docs here say “bech32m addresses”, but the SDK implementation uses cashaddr-style hrp: addresses. Please make the statics network documentation consistent with the actual address format used elsewhere in the PR.

Copilot uses AI. Check for mistakes.
Comment thread modules/bitgo/src/v2/coins/index.ts Outdated
Comment on lines +37 to +117
@@ -113,6 +114,7 @@ export { Etc, Tetc };
export { EvmCoin, EthLikeErc20Token, EthLikeErc721Token };
export { Flr, Tflr, FlrToken };
export { Flrp };
export { Kas, TKas };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TKas should be Tkas
this the naming convention followed for other coins

Comment thread modules/bitgo/src/v2/coinFactory.ts Outdated
Flr,
Flrp,
Kas,
TKas,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tkas

Comment on lines +55 to +61
// RPC URLs (wRPC WebSocket)
export const MAINNET_NODE_URL = 'wss://mainnet.kaspa.green';
export const TESTNET_NODE_URL = 'wss://testnet-10.kaspa.green';

// Explorer URLs
export const MAINNET_EXPLORER_URL = 'https://explorer.kaspa.org/txs/';
export const TESTNET_EXPLORER_URL = 'https://explorer-tn10.kaspa.org/txs/';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if they can be picked from configs/statics

Comment on lines +22 to +23
/** BLAKE2B output size (32 bytes) */
const HASH_SIZE = 32;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was in constant.ts, use from there

Comment thread modules/sdk-coin-kas/src/tkas.ts Outdated
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { Kas } from './kas';

export class TKas extends Kas {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tkas

Comment on lines -9366 to -9367
'kas',
'Kaspa',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a existing token, why changing this?

Comment thread modules/bitgo/src/v2/coins/index.ts Outdated
import { EvmCoin, EthLikeErc20Token, EthLikeErc721Token } from '@bitgo/sdk-coin-evm';
import { Flr, Tflr, FlrToken } from '@bitgo/sdk-coin-flr';
import { Flrp } from '@bitgo/sdk-coin-flrp';
import { Kas, TKas } from '@bitgo/sdk-coin-kas';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for this as well. It needs to be Tkas

Comment thread modules/bitgo/src/v2/coinFactory.ts Outdated
coinFactory.register('tfiatusd', TfiatUsd.createInstance);
coinFactory.register('tflr', Tflr.createInstance);
coinFactory.register('tflrp', Flrp.createInstance);
coinFactory.register('tkas', TKas.createInstance);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread modules/bitgo/src/v2/coinFactory.ts Outdated
case 'tflrp':
return Flrp.createInstance;
case 'tkas':
return TKas.createInstance;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread modules/sdk-coin-kas/src/kas.ts Outdated
Comment on lines +227 to +234
async signMessage(key: BaseKeyPair, message: string | Buffer): Promise<Buffer> {
throw new MethodNotImplementedError();
}

/** @inheritdoc */
auditDecryptedKey(params: AuditDecryptedKeyParams): void {
throw new MethodNotImplementedError();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not throw an error for non implemented methods rather just return it.
It is avoid any issues with TAT that we faced in the past.

Comment thread modules/sdk-coin-kas/src/kas.ts Outdated
Comment on lines +98 to +103
try {
new KeyPair({ pub });
return true;
} catch {
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to utils

Comment thread modules/sdk-coin-kas/src/kas.ts Outdated
Comment on lines +110 to +115
try {
new KeyPair({ prv });
return true;
} catch {
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manojkumar138 manojkumar138 force-pushed the feat/kaspa-sdk branch 5 times, most recently from f9e9eb2 to 6dca045 Compare April 10, 2026 10:41
Copy link
Copy Markdown
Contributor

@Marzooqa Marzooqa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flush, failing CI and merge conflicts

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +36
it('should return "kas" for mainnet', () => {
kas.getChain().should.equal('kaspa');
});

it('should return "tkas" for testnet', () => {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getChain test descriptions say it should return "kas" / "tkas", but the assertions (and statics coin names) are "kaspa" / "tkaspa". Please align the test descriptions with the expected values to avoid misleading failures/debugging later.

Suggested change
it('should return "kas" for mainnet', () => {
kas.getChain().should.equal('kaspa');
});
it('should return "tkas" for testnet', () => {
it('should return "kaspa" for mainnet', () => {
kas.getChain().should.equal('kaspa');
});
it('should return "tkaspa" for testnet', () => {

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +164
if (kasParams.txPrebuild?.utxoEntries?.length) {
txBuilder.addUtxos(kasParams.txPrebuild.utxoEntries);
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signTransaction calls txBuilder.addUtxos(kasParams.txPrebuild.utxoEntries) after from(txHex). addUtxos() appends new inputs/UTXO entries rather than attaching UTXO metadata to the already-deserialized inputs, which will change the transaction being signed (duplicate inputs / altered change calculation) and can produce an invalid signature or invalid tx. Consider adding a dedicated API on the builder/transaction to set utxoEntries for existing inputs (by index) without mutating inputs, or pass utxoEntries into from()/sign() and have computeSighash reference that data.

Suggested change
if (kasParams.txPrebuild?.utxoEntries?.length) {
txBuilder.addUtxos(kasParams.txPrebuild.utxoEntries);
}
// Do not call addUtxos() after from(txHex): addUtxos appends inputs/UTXO entries
// and would mutate the transaction being signed.

Copilot uses AI. Check for mistakes.

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
return this.build();
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class overrides build() with custom logic and then has buildImplementation() delegate to this.build(). Because of method overriding, callers invoking builder.build() will hit the custom build() and bypass BaseTransactionBuilder.build() (which enforces validateTransaction(...) before calling buildImplementation). Consider removing/renaming the custom build() method and moving its logic into buildImplementation() so the base class validation flow is preserved.

Suggested change
return this.build();
return this._transaction;

Copilot uses AI. Check for mistakes.
Comment on lines +275 to +292
async build(): Promise<Transaction> {
if (this._utxoEntries.length > 0) {
// Calculate total input and output amounts
const totalIn = this._utxoEntries.reduce((sum, u) => sum + u.amount, BigInt(0));
const totalOut = this._outputs.reduce((sum, o) => sum + o.value, BigInt(0));
const change = totalIn - totalOut - this._fee;

// Add change output if there is any change
const changeAddr = this._changeAddress || this._sender;
if (change > BigInt(0)) {
if (!changeAddr) {
throw new BuildTransactionError('Change address or sender address required for change output');
}
this._outputs.push({
value: change,
scriptPublicKey: addressToScriptPublicKey(changeAddr),
});
} else if (change < BigInt(0)) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build() mutates _outputs by pushing a change output in-place. If build() is called multiple times (or fee/recipients are adjusted after a build), the previously-added change output becomes part of the next totalOut calculation and can lead to incorrect change or duplicated/miscomputed outputs. Consider keeping “user outputs” separate from computed change, or rebuilding a fresh outputs array each build (e.g., compute change without mutating the stored recipient outputs).

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +209
return blake2bWithTag('TransactionSigningHash', Buffer.concat(parts));
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computeSighash hardcodes the domain tag string 'TransactionSigningHash' even though the module defines SIGHASH_DOMAIN_TAG in constants.ts. Using the exported constant avoids drift if the tag ever changes (and keeps the implementation consistent with the declared constants).

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +124
const dataWithoutChecksum = words.slice(0, -8);
const decoded = kaspaConvertBits(dataWithoutChecksum, 5, 8, false);
const version = decoded[0];
const payload = Buffer.from(decoded.slice(1));
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kaspaDecodeAddress calls kaspaConvertBits(..., pad=false), but kaspaConvertBits does not validate leftover/non-zero padding bits when pad is false. This can allow multiple distinct (checksum-valid) address strings to decode to the same payload (non-canonical encodings). Consider adding the standard convert-bits strictness checks during decode and rejecting inputs with invalid padding.

Copilot uses AI. Check for mistakes.
Comment thread modules/sdk-coin-kas/src/lib/utils.ts Outdated
Comment on lines +339 to +340
* Serialize a transaction ID (reverse byte order, like Bitcoin).
* Note: Kaspa does NOT reverse transaction IDs unlike Bitcoin.
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above serializeTxId says to reverse byte order like Bitcoin, but the implementation intentionally does not reverse for Kaspa. Updating the comment to remove the Bitcoin reversal mention (or clarifying that no reversal is performed) will prevent confusion for future maintainers.

Suggested change
* Serialize a transaction ID (reverse byte order, like Bitcoin).
* Note: Kaspa does NOT reverse transaction IDs unlike Bitcoin.
* Serialize a transaction ID without reversing byte order.
* Kaspa transaction IDs are serialized as provided, unlike Bitcoin.

Copilot uses AI. Check for mistakes.
Comment on lines +340 to +342
const val = Number(buf.readBigUInt64LE(offset));
offset += 8;
return val;
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readVarInt() converts the 0xff case to a JS number via Number(buf.readBigUInt64LE(...)). Values above Number.MAX_SAFE_INTEGER will lose precision and can lead to incorrect lengths/offsets when deserializing untrusted transaction data. Consider keeping varint values as bigint (and enforcing sane upper bounds) or explicitly throw if the decoded value exceeds Number.MAX_SAFE_INTEGER.

Suggested change
const val = Number(buf.readBigUInt64LE(offset));
offset += 8;
return val;
const val = buf.readBigUInt64LE(offset);
offset += 8;
if (val > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error('VarInt exceeds Number.MAX_SAFE_INTEGER');
}
return Number(val);

Copilot uses AI. Check for mistakes.
@manojkumar138 manojkumar138 force-pushed the feat/kaspa-sdk branch 2 times, most recently from 29583cd to 61ed4a6 Compare April 14, 2026 10:17
…HO-388]

- New sdk-coin-kas module with KeyPair, Transaction, TransactionBuilder
- Kaspa cashaddr address encoding/decoding with BLAKE2B checksums
- Schnorr signing via @noble/curves/secp256k1 with SIGHASH_ALL
- BIP143-style sighash using BLAKE2B with TransactionSigningHash tag
- Register kaspa/tkaspa coins; CoinFamily.KAS = 'kaspa'
- setUtxoEntries() for post-deserialization UTXO attachment
- buildImplementation() uses local finalOutputs (no mutation on rebuild)
- readVarInt precision guard for values >Number.MAX_SAFE_INTEGER
- Non-canonical padding validation in kaspaDecodeAddress

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


/** Signatures on this transaction (as hex strings) */
get signature(): string[] {
return this._txData.inputs.map((i) => i.signatureScript).filter(Boolean);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transaction.signature currently returns one entry per input signatureScript. In this repo, signature.length is used to decide whether signTransaction returns txHex vs halfSigned, where the length represents the number of signer signatures applied (e.g., threshold 2) rather than the number of inputs. With the current implementation a single-signer transaction with 1 input will always look "half-signed", while 2 inputs will look "fully signed" even though only one signer was applied. Consider tracking signer signatures separately (or making signature represent the number of signer sets applied), so the halfSigned logic behaves consistently across coins.

Suggested change
return this._txData.inputs.map((i) => i.signatureScript).filter(Boolean);
// For Kaspa in this SDK, `signature.length` is used by signing flows to
// represent how many signer sets have been applied to the transaction,
// not how many individual inputs currently carry a signatureScript.
//
// Returning one entry per signed input makes the result depend on input
// count: a single signer on one input looks "half-signed", while the same
// single signer on multiple inputs can incorrectly look "fully signed".
//
// Treat any present input signatures as a single applied signer set and
// return at most one representative signature to keep `.length`
// consistent with signer progress for this single-signer implementation.
const signedInput = this._txData.inputs.find((i) => !!i.signatureScript);
return signedInput?.signatureScript ? [signedInput.signatureScript] : [];

Copilot uses AI. Check for mistakes.
Comment thread modules/sdk-coin-kas/src/kas.ts Outdated
Comment on lines +124 to +146
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
const { address, keychains } = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`Invalid address: ${address}`);
}
if (!keychains || keychains.length === 0) {
throw new Error('No keychains provided');
}

// For single-sig: check that the address matches one of the keychains
const networkType = utils.isMainnetAddress(address) ? 'mainnet' : 'testnet';
const derivedAddresses = keychains.map((kc) => {
const kp = new KeyPair({ pub: kc.pub });
return kp.getAddress(networkType);
});

if (!derivedAddresses.includes(address)) {
throw new UnexpectedAddressError(`Address ${address} does not match any keychain`);
}

return true;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isWalletAddress only supports VerifyAddressOptions with keychains[].pub, but this coin advertises TSS support and SDK callers may pass TssVerifyAddressOptions (with keychains[].commonKeychain + index). As written, TSS address verification will fail with "No keychains provided" or derive against the wrong key material. Consider adding an isTssVerifyAddressOptions(params) branch and using verifyMPCWalletAddress (sdk-core) to derive/verify the address for MPC wallets, similar to other coins.

Copilot uses AI. Check for mistakes.
Comment thread modules/sdk-coin-kas/src/kas.ts Outdated
* Sign a message with a key pair.
*/
async signMessage(_key: BaseKeyPair, _message: string | Buffer): Promise<Buffer> {
return Buffer.alloc(0);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signMessage currently returns an empty buffer, which can be mistaken by callers as a valid signature. If Kaspa message signing is not supported yet, it should throw a MethodNotImplementedError/SigningError (consistent with other coin implementations) to avoid silently producing invalid signatures.

Suggested change
return Buffer.alloc(0);
throw new SigningError('Kaspa message signing is not supported');

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +41
/**
* Compute a BLAKE2B hash with a Kaspa domain tag prefix.
* Kaspa prepends each hash with: BLAKE2B(domain_tag) || data
*/
function blake2bWithTag(tag: string, data: Buffer): Buffer {
// Kaspa uses a tagged hash: H(tag_length || tag || data)
// The tag is hashed separately and prepended as a "personalization"
const tagBuf = Buffer.from(tag, 'utf8');
const hasher = blake2b.create({ dkLen: HASH_SIZE });
hasher.update(tagBuf);
hasher.update(data);
return Buffer.from(hasher.digest());
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments describing Kaspa tagged hashing (Kaspa prepends each hash with: BLAKE2B(domain_tag) || data / personalization) don’t match the implementation in blake2bWithTag, which currently hashes tag || data directly. Please either update the implementation to match the documented tagged-hash scheme used by Kaspa, or adjust the comments so they accurately describe what is being hashed.

Copilot uses AI. Check for mistakes.
@mohd-kashif mohd-kashif marked this pull request as draft April 14, 2026 18:40
Copy link
Copy Markdown
Contributor

@mrdanish26 mrdanish26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushing the queue, draft stage

- signature getter: return at most one entry to correctly track signer
  count vs input count (fixes halfSigned/txHex logic)
- isWalletAddress: add TSS branch using verifyMPCWalletAddress secp256k1
- signMessage: throw SigningError instead of returning empty buffer
- blake2bWithTag: fix comment to match actual tag||data hashing
- kaspaConvertBits: use BigInt for maxv to avoid 32-bit truncation
- devDeps: replace should with @bitgo/sdk-test; add sdk-test tsconfig ref
- Resolve merge conflicts; bump deps to match master versions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants