feat(sdk-coin-kas): add Kaspa (KAS) unique chain SDK integration#8421
feat(sdk-coin-kas): add Kaspa (KAS) unique chain SDK integration#8421manojkumar138 wants to merge 1 commit intomasterfrom
Conversation
00809c6 to
4e4bc10
Compare
4e4bc10 to
5e47526
Compare
There was a problem hiding this comment.
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/tkasin the global coin/token lists. - Introduced
@bitgo/sdk-coin-kaswith 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
ekasto 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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'); | |
| } |
| * Kaspa (KAS) Utility Functions | ||
| * | ||
| * Address validation, encoding, script generation, and other utilities. | ||
| * Kaspa uses bech32m-encoded addresses with x-only secp256k1 public keys. |
There was a problem hiding this comment.
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.
| * 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. |
| /** | ||
| * 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); | ||
| } |
There was a problem hiding this comment.
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.
| const sig = secp256k1.sign(sighash, privKeyBytes, { lowS: false }); | ||
| const sigBytes = Buffer.from(sig.toCompactRawBytes()); |
There was a problem hiding this comment.
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.
| const sig = secp256k1.sign(sighash, privKeyBytes, { lowS: false }); | |
| const sigBytes = Buffer.from(sig.toCompactRawBytes()); | |
| const sigBytes = Buffer.from(secp256k1.schnorr.sign(sighash, privKeyBytes)); |
| 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 } }; |
There was a problem hiding this comment.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
@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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
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.publicKey → TEST_ACCOUNT.mainnetAddress) to catch encoding/signing regressions.
| /** | ||
| * 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; | ||
| } |
There was a problem hiding this comment.
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.
modules/bitgo/src/v2/coins/index.ts
Outdated
| @@ -113,6 +114,7 @@ export { Etc, Tetc }; | |||
| export { EvmCoin, EthLikeErc20Token, EthLikeErc721Token }; | |||
| export { Flr, Tflr, FlrToken }; | |||
| export { Flrp }; | |||
| export { Kas, TKas }; | |||
There was a problem hiding this comment.
TKas should be Tkas
this the naming convention followed for other coins
modules/bitgo/src/v2/coinFactory.ts
Outdated
| Flr, | ||
| Flrp, | ||
| Kas, | ||
| TKas, |
| // 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/'; |
There was a problem hiding this comment.
check if they can be picked from configs/statics
| /** BLAKE2B output size (32 bytes) */ | ||
| const HASH_SIZE = 32; |
There was a problem hiding this comment.
this was in constant.ts, use from there
modules/sdk-coin-kas/src/tkas.ts
Outdated
| import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; | ||
| import { Kas } from './kas'; | ||
|
|
||
| export class TKas extends Kas { |
| 'kas', | ||
| 'Kaspa', |
There was a problem hiding this comment.
this is a existing token, why changing this?
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'; |
There was a problem hiding this comment.
same for this as well. It needs to be Tkas
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); |
There was a problem hiding this comment.
modules/bitgo/src/v2/coinFactory.ts
Outdated
| case 'tflrp': | ||
| return Flrp.createInstance; | ||
| case 'tkas': | ||
| return TKas.createInstance; |
There was a problem hiding this comment.
modules/sdk-coin-kas/src/kas.ts
Outdated
| async signMessage(key: BaseKeyPair, message: string | Buffer): Promise<Buffer> { | ||
| throw new MethodNotImplementedError(); | ||
| } | ||
|
|
||
| /** @inheritdoc */ | ||
| auditDecryptedKey(params: AuditDecryptedKeyParams): void { | ||
| throw new MethodNotImplementedError(); | ||
| } |
There was a problem hiding this comment.
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.
modules/sdk-coin-kas/src/kas.ts
Outdated
| try { | ||
| new KeyPair({ pub }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
modules/sdk-coin-kas/src/kas.ts
Outdated
| try { | ||
| new KeyPair({ prv }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
f1da067 to
f9e9eb2
Compare
…HO-388] Implements a full BitGoJS SDK module for Kaspa (KAS), a UTXO-based BlockDAG chain using Schnorr signatures over secp256k1 and BLAKE2B sighash with the "TransactionSigningHash" domain tag. Changes: - modules/sdk-coin-kas/: new SDK module with KeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory, Kas/TKas coin classes, constants, interfaces, and utilities - Kaspa cashaddr encoding (`:` separator, custom polymod checksum) - statics: KasCoin class, CoinFamily.KAS, BaseUnit.KAS, KaspaMainnet/ KaspaTestnet networks, kas/tkas coin entries with UUIDs - sdk-core/environments.ts: kasNodeUrl field - bitgo/coinFactory.ts + coins/index.ts: kas/tkas registered - account-lib: kas/tkas added to coinBuilderMap - tsconfig references updated (account-lib, bitgo, tsconfig.packages.json) - CODEOWNERS, Dockerfile updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f9e9eb2 to
6dca045
Compare
No description provided.