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 2 commits 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.
| @@ -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
| 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
| 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?
| 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
| 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.
| case 'tflrp': | ||
| return Flrp.createInstance; | ||
| case 'tkas': | ||
| return TKas.createInstance; |
There was a problem hiding this comment.
| 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.
| try { | ||
| new KeyPair({ pub }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
| try { | ||
| new KeyPair({ prv }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
f9e9eb2 to
6dca045
Compare
Marzooqa
left a comment
There was a problem hiding this comment.
flush, failing CI and merge conflicts
4fd7cdd to
91f655e
Compare
There was a problem hiding this comment.
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.
| it('should return "kas" for mainnet', () => { | ||
| kas.getChain().should.equal('kaspa'); | ||
| }); | ||
|
|
||
| it('should return "tkas" for testnet', () => { |
There was a problem hiding this comment.
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.
| 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', () => { |
| if (kasParams.txPrebuild?.utxoEntries?.length) { | ||
| txBuilder.addUtxos(kasParams.txPrebuild.utxoEntries); | ||
| } |
There was a problem hiding this comment.
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.
| 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. |
|
|
||
| /** @inheritdoc */ | ||
| protected async buildImplementation(): Promise<Transaction> { | ||
| return this.build(); |
There was a problem hiding this comment.
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.
| return this.build(); | |
| return this._transaction; |
| 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)) { |
There was a problem hiding this comment.
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).
| return blake2bWithTag('TransactionSigningHash', Buffer.concat(parts)); | ||
| } |
There was a problem hiding this comment.
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).
| const dataWithoutChecksum = words.slice(0, -8); | ||
| const decoded = kaspaConvertBits(dataWithoutChecksum, 5, 8, false); | ||
| const version = decoded[0]; | ||
| const payload = Buffer.from(decoded.slice(1)); |
There was a problem hiding this comment.
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.
| * Serialize a transaction ID (reverse byte order, like Bitcoin). | ||
| * Note: Kaspa does NOT reverse transaction IDs unlike Bitcoin. |
There was a problem hiding this comment.
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.
| * 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. |
| const val = Number(buf.readBigUInt64LE(offset)); | ||
| offset += 8; | ||
| return val; |
There was a problem hiding this comment.
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.
| 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); |
29583cd to
61ed4a6
Compare
…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>
61ed4a6 to
fb8514c
Compare
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| 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] : []; |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| * Sign a message with a key pair. | ||
| */ | ||
| async signMessage(_key: BaseKeyPair, _message: string | Buffer): Promise<Buffer> { | ||
| return Buffer.alloc(0); |
There was a problem hiding this comment.
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.
| return Buffer.alloc(0); | |
| throw new SigningError('Kaspa message signing is not supported'); |
| /** | ||
| * 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()); |
There was a problem hiding this comment.
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.
mrdanish26
left a comment
There was a problem hiding this comment.
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>
No description provided.