Skip to content

Commit 436c58d

Browse files
committed
feat: kaspa support
ticket: cecho-1071
1 parent 7b84f76 commit 436c58d

17 files changed

Lines changed: 2188 additions & 385 deletions

modules/sdk-coin-kaspa/README.md

Lines changed: 224 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Supported coin identifiers:
1717
- Address derivation (kaspa bech32 P2PK Schnorr)
1818
- UTXO transaction building
1919
- Schnorr signing and verification (Blake2b-256 sighash)
20-
- TSS/MPC support (ECDSA algorithm)
20+
- TSS/MPC support (ECDSA algorithm, per-input DKLS sessions)
2121
- Full serialization round-trip (hex/JSON)
2222

2323
## Installation
@@ -28,82 +28,273 @@ yarn add @bitgo/sdk-coin-kaspa
2828

2929
## Usage
3030

31-
### Register with BitGo SDK
31+
### 1. Register with BitGo SDK
3232

3333
```typescript
34+
import { BitGo } from 'bitgo';
3435
import { register } from '@bitgo/sdk-coin-kaspa';
36+
37+
const bitgo = new BitGo({ env: 'prod' });
3538
register(bitgo);
39+
40+
const kaspa = bitgo.coin('kaspa');
41+
const tkaspa = bitgo.coin('tkaspa'); // testnet
42+
```
43+
44+
Alternatively, instantiate directly (useful in tests or scripts):
45+
46+
```typescript
47+
import { Kaspa, Tkaspa } from '@bitgo/sdk-coin-kaspa';
48+
49+
const kaspa = Kaspa.createInstance(bitgo);
50+
const tkaspa = Tkaspa.createInstance(bitgo);
51+
```
52+
53+
---
54+
55+
### 2. Key Generation
56+
57+
```typescript
58+
// Random key pair
59+
const kp = kaspa.generateKeyPair();
60+
console.log(kp.pub); // 66-char hex compressed secp256k1 public key
61+
console.log(kp.prv); // 64-char hex private key
62+
63+
// Deterministic from a 32-byte seed
64+
const seed = Buffer.from('...32 bytes...', 'hex');
65+
const kpFromSeed = kaspa.generateKeyPair(seed);
66+
67+
// Validation
68+
kaspa.isValidPub(kp.pub); // true
69+
kaspa.isValidPrv(kp.prv); // true
70+
```
71+
72+
Using `KeyPair` directly:
73+
74+
```typescript
75+
import { KeyPair } from '@bitgo/sdk-coin-kaspa';
76+
77+
const keyPair = new KeyPair(); // random
78+
const { pub, prv } = keyPair.getKeys();
3679
```
3780

38-
### Key Pair
81+
---
82+
83+
### 3. Address Generation
3984

4085
```typescript
4186
import { KeyPair } from '@bitgo/sdk-coin-kaspa';
4287

43-
// Generate a random key pair
44-
const kp = new KeyPair();
45-
const { pub, prv } = kp.getKeys();
88+
const keyPair = new KeyPair({ prv: '<64-char-hex-private-key>' });
89+
90+
const mainnetAddress = keyPair.getAddress('mainnet'); // kaspa:qq...
91+
const testnetAddress = keyPair.getAddress('testnet'); // kaspatest:qq...
92+
93+
kaspa.isValidAddress(mainnetAddress); // true
94+
```
95+
96+
Computing the P2PK `scriptPublicKey` for a key (required when constructing UTXO inputs):
4697

47-
// Derive address
48-
const mainnetAddress = kp.getAddress('mainnet');
49-
const testnetAddress = kp.getAddress('testnet');
98+
```typescript
99+
import { compressedToXOnly, buildP2PKScriptPublicKey } from '@bitgo/sdk-coin-kaspa';
100+
101+
const xOnlyPub = compressedToXOnly(Buffer.from(pub, 'hex'));
102+
const scriptPublicKey = buildP2PKScriptPublicKey(xOnlyPub).toString('hex');
50103
```
51104

52-
### Build and Sign a Transaction
105+
---
106+
107+
### 4. Building a Transaction
53108

54109
```typescript
55-
import { TransactionBuilderFactory } from '@bitgo/sdk-coin-kaspa';
110+
import { TransactionBuilderFactory, Transaction } from '@bitgo/sdk-coin-kaspa';
56111
import { coins } from '@bitgo/statics';
112+
import type { KaspaUtxoInput } from '@bitgo/sdk-coin-kaspa';
113+
114+
const utxo: KaspaUtxoInput = {
115+
transactionId: '<64-char-hex-prev-tx-id>',
116+
transactionIndex: 0,
117+
amount: '100000000', // 1 KASPA in sompi (1e8)
118+
scriptPublicKey: '<hex>', // P2PK script of the sender's key
119+
sequence: '0',
120+
sigOpCount: 1,
121+
};
57122

58123
const factory = new TransactionBuilderFactory(coins.get('kaspa'));
59124
const builder = factory.getBuilder();
60125

61126
builder
62-
.addInput({
63-
transactionId: '<prev-tx-id>',
64-
transactionIndex: 0,
65-
amount: '100000000', // 1 KASPA in sompi
66-
scriptPublicKey: '<spk>',
67-
sequence: '0',
68-
sigOpCount: 1,
69-
})
70-
.to('<recipient-kaspa-address>', '99998000')
127+
.addInput(utxo)
128+
.to('kaspa:qq...recipient...', '99998000') // amount in sompi
71129
.fee('2000');
72130

73-
const tx = await builder.build();
131+
const tx = (await builder.build()) as Transaction;
132+
```
133+
134+
Multiple inputs:
135+
136+
```typescript
137+
builder
138+
.addInput(utxo1)
139+
.addInput(utxo2)
140+
.to(recipientAddress, '299996000')
141+
.fee('4000');
142+
```
143+
144+
---
145+
146+
### 5. Signing — Path A: Direct Private Key (non-TSS)
147+
148+
```typescript
149+
// `tx.sign` takes a 32-byte Buffer (raw private key)
74150
tx.sign(Buffer.from(privateKeyHex, 'hex'));
75151

76-
const broadcastPayload = tx.toBroadcastFormat(); // JSON string for RPC
152+
// Signs every input at once. Fully signed → txHex; partial → halfSigned
153+
const signedTxHex = tx.toHex(); // SDK-internal format for round-trips
154+
155+
// Or via the coin interface:
156+
const result = await kaspa.signTransaction({
157+
txPrebuild: { txHex: unsignedTxHex },
158+
prv: privateKeyHex,
159+
} as any) as { txHex: string };
77160
```
78161

162+
---
163+
164+
### 6. Signing — Path B: TSS / MPC (per-input DKLS sessions)
165+
166+
Kaspa is UTXO-based: every input has its own sighash (Blake2b-256, BIP-143-like).
167+
Each input **requires an independent DKLS session** — there is no way to produce N valid
168+
Schnorr signatures from a single signing operation.
169+
170+
```typescript
171+
const unsignedTx = (await builder.build()) as Transaction;
172+
const txHex = unsignedTx.toHex();
173+
174+
// Step 1: one sighash Buffer per input — the messages for each DKLS session
175+
const sighashes: Buffer[] = unsignedTx.signablePayloads; // Buffer[N]
176+
177+
// Step 2: run N DKLS sessions in parallel (one per sighash)
178+
// Each session produces a 64-byte raw Schnorr signature
179+
180+
// Step 3: collect results and call signTransaction
181+
const signatures = sighashes.map((hash, inputIndex) => ({
182+
inputIndex,
183+
pubKey: compressedPubKeyHex, // 33-byte hex
184+
signature: dklsSession(hash), // 64-byte hex Schnorr sig
185+
}));
186+
187+
const result = await kaspa.signTransaction({
188+
txPrebuild: { txHex },
189+
signatures,
190+
} as any) as { txHex: string } | { halfSigned: { txHex: string } };
191+
192+
// result.txHex → all inputs signed
193+
// result.halfSigned → some inputs still unsigned (partial TSS round)
194+
```
195+
196+
---
197+
198+
### 7. Broadcasting
199+
200+
```typescript
201+
// toBroadcastFormat() returns the Kaspa RPC-compatible JSON string
202+
const broadcastPayload = tx.toBroadcastFormat();
203+
204+
// toHex() is the SDK-internal round-trip format (preserves amount + scriptPublicKey
205+
// on inputs for sighash recomputation). Do NOT send this to the Kaspa node directly.
206+
const internalHex = tx.toHex();
207+
```
208+
209+
---
210+
211+
### 8. Explaining / Parsing a Transaction
212+
213+
```typescript
214+
// Human-readable breakdown
215+
const explained = await kaspa.explainTransaction({ txHex });
216+
console.log(explained.outputs); // [{ address, amount }]
217+
console.log(explained.outputAmount); // total sent (sompi)
218+
console.log(explained.fee); // fee (sompi)
219+
220+
// Structured parse — inputs and outputs tagged with coin name
221+
const parsed = await kaspa.parseTransaction({ txHex } as any);
222+
// { inputs: [{ amount, coin: 'kaspa' }], outputs: [{ address, amount, coin: 'kaspa' }] }
223+
```
224+
225+
---
226+
227+
### 9. Verifying a Transaction
228+
229+
```typescript
230+
const valid = await kaspa.verifyTransaction({
231+
txPrebuild: { txHex },
232+
txParams: {
233+
recipients: [{ address: 'kaspa:qq...', amount: '99998000' }],
234+
},
235+
} as any);
236+
237+
console.log(valid); // true
238+
```
239+
240+
---
241+
242+
### 10. Coin Properties
243+
244+
```typescript
245+
kaspa.getChain(); // 'kaspa'
246+
kaspa.getFamily(); // 'kaspa'
247+
kaspa.getFullName(); // 'Kaspa'
248+
kaspa.getBaseFactor(); // 100_000_000 (sompi per KASPA)
249+
kaspa.supportsTss(); // true
250+
kaspa.getMPCAlgorithm(); // 'ecdsa'
251+
252+
tkaspa.getChain(); // 'tkaspa'
253+
tkaspa.getFullName(); // 'Testnet Kaspa'
254+
```
255+
256+
---
257+
258+
## Key Constants
259+
260+
| Property | Value |
261+
|---|---|
262+
| 1 KASPA | `100_000_000` sompi |
263+
| `getBaseFactor()` | `1e8` |
264+
| Mainnet address prefix | `kaspa:` |
265+
| Testnet address prefix | `kaspatest:` |
266+
| Address type | P2PK Schnorr (x-only secp256k1) |
267+
| Signature algorithm | Schnorr (Blake2b-256 sighash) |
268+
| TSS algorithm | `ecdsa` (DKLS) |
269+
| Multisig type | `onchain` |
270+
271+
---
272+
79273
## Module Structure
80274

81275
```
82276
src/
83-
├── kaspa.ts # Kaspa mainnet coin class
84-
├── tkaspa.ts # Kaspa testnet coin class
85-
├── register.ts # SDK registration
277+
├── kaspa.ts # AbstractKaspaLikeCoin, Kaspa, Tkaspa classes
278+
├── register.ts # SDK registration helper
86279
├── index.ts
87280
└── lib/
88-
├── constants.ts # Chain constants (prefixes, decimals, fees)
281+
├── constants.ts # Chain constants (prefixes, decimals, default fee)
89282
├── iface.ts # TypeScript interfaces
90-
├── keyPair.ts # secp256k1 key pair
91-
├── sighash.ts # Blake2b-256 Schnorr sighash
92-
├── transaction.ts # Transaction class (sign/verify/explain)
283+
├── keyPair.ts # secp256k1 key pair + address derivation
284+
├── sighash.ts # Blake2b-256 Schnorr sighash + script utilities
285+
├── transaction.ts # Transaction class (sign / verify / explain / serialize)
93286
├── transactionBuilder.ts # UTXO transaction builder
94287
├── transactionBuilderFactory.ts
95288
├── utils.ts # Address validation and encoding
96289
└── index.ts
97290
test/
98291
├── fixtures/
99-
│ ├── kaspa.fixtures.ts # Deterministic test vectors
100-
│ └── kaspaFixtures.ts # Synthetic test fixtures
292+
│ └── kaspa.fixtures.ts # Deterministic test vectors
101293
└── unit/
102294
├── coin.test.ts
103295
├── keyPair.test.ts
104296
├── transaction.test.ts
105297
├── transactionBuilder.test.ts
106-
├── transactionFlow.test.ts
107298
└── utils.test.ts
108299
```
109300

@@ -117,7 +308,7 @@ Kaspa uses a custom cashaddr-like bech32 encoding:
117308

118309
## Signing
119310

120-
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.
311+
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 (`0x01` = SIGHASH_ALL).
121312

122313
## References
123314

modules/sdk-coin-kaspa/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
]
4141
},
4242
"dependencies": {
43+
"@bitgo/abstract-utxo": "^11.0.0",
4344
"@bitgo/sdk-core": "^36.44.0",
4445
"@bitgo/secp256k1": "^1.11.0",
4546
"@bitgo/statics": "^58.39.0",

0 commit comments

Comments
 (0)