@sequence0/sdk 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/dist/chains/algorand.d.ts +44 -0
  3. package/dist/chains/algorand.d.ts.map +1 -0
  4. package/dist/chains/algorand.js +148 -0
  5. package/dist/chains/algorand.js.map +1 -0
  6. package/dist/chains/aptos.d.ts +39 -0
  7. package/dist/chains/aptos.d.ts.map +1 -0
  8. package/dist/chains/aptos.js +168 -0
  9. package/dist/chains/aptos.js.map +1 -0
  10. package/dist/chains/cardano.d.ts +42 -0
  11. package/dist/chains/cardano.d.ts.map +1 -0
  12. package/dist/chains/cardano.js +188 -0
  13. package/dist/chains/cardano.js.map +1 -0
  14. package/dist/chains/cosmos.d.ts +42 -0
  15. package/dist/chains/cosmos.d.ts.map +1 -0
  16. package/dist/chains/cosmos.js +216 -0
  17. package/dist/chains/cosmos.js.map +1 -0
  18. package/dist/chains/dogecoin-litecoin.d.ts +57 -0
  19. package/dist/chains/dogecoin-litecoin.d.ts.map +1 -0
  20. package/dist/chains/dogecoin-litecoin.js +521 -0
  21. package/dist/chains/dogecoin-litecoin.js.map +1 -0
  22. package/dist/chains/ethereum.d.ts.map +1 -1
  23. package/dist/chains/ethereum.js +16 -0
  24. package/dist/chains/ethereum.js.map +1 -1
  25. package/dist/chains/hedera.d.ts +113 -0
  26. package/dist/chains/hedera.d.ts.map +1 -0
  27. package/dist/chains/hedera.js +302 -0
  28. package/dist/chains/hedera.js.map +1 -0
  29. package/dist/chains/icp.d.ts +95 -0
  30. package/dist/chains/icp.d.ts.map +1 -0
  31. package/dist/chains/icp.js +520 -0
  32. package/dist/chains/icp.js.map +1 -0
  33. package/dist/chains/kaspa.d.ts +152 -0
  34. package/dist/chains/kaspa.d.ts.map +1 -0
  35. package/dist/chains/kaspa.js +790 -0
  36. package/dist/chains/kaspa.js.map +1 -0
  37. package/dist/chains/multiversx.d.ts +143 -0
  38. package/dist/chains/multiversx.d.ts.map +1 -0
  39. package/dist/chains/multiversx.js +524 -0
  40. package/dist/chains/multiversx.js.map +1 -0
  41. package/dist/chains/near.d.ts +40 -0
  42. package/dist/chains/near.d.ts.map +1 -0
  43. package/dist/chains/near.js +170 -0
  44. package/dist/chains/near.js.map +1 -0
  45. package/dist/chains/polkadot.d.ts +43 -0
  46. package/dist/chains/polkadot.d.ts.map +1 -0
  47. package/dist/chains/polkadot.js +179 -0
  48. package/dist/chains/polkadot.js.map +1 -0
  49. package/dist/chains/ripple.d.ts +41 -0
  50. package/dist/chains/ripple.d.ts.map +1 -0
  51. package/dist/chains/ripple.js +190 -0
  52. package/dist/chains/ripple.js.map +1 -0
  53. package/dist/chains/stellar.d.ts +40 -0
  54. package/dist/chains/stellar.d.ts.map +1 -0
  55. package/dist/chains/stellar.js +156 -0
  56. package/dist/chains/stellar.js.map +1 -0
  57. package/dist/chains/sui.d.ts +44 -0
  58. package/dist/chains/sui.d.ts.map +1 -0
  59. package/dist/chains/sui.js +157 -0
  60. package/dist/chains/sui.js.map +1 -0
  61. package/dist/chains/tezos.d.ts +43 -0
  62. package/dist/chains/tezos.d.ts.map +1 -0
  63. package/dist/chains/tezos.js +162 -0
  64. package/dist/chains/tezos.js.map +1 -0
  65. package/dist/chains/ton.d.ts +40 -0
  66. package/dist/chains/ton.d.ts.map +1 -0
  67. package/dist/chains/ton.js +168 -0
  68. package/dist/chains/ton.js.map +1 -0
  69. package/dist/chains/tron.d.ts +41 -0
  70. package/dist/chains/tron.d.ts.map +1 -0
  71. package/dist/chains/tron.js +124 -0
  72. package/dist/chains/tron.js.map +1 -0
  73. package/dist/core/atomic.d.ts +76 -0
  74. package/dist/core/atomic.d.ts.map +1 -0
  75. package/dist/core/atomic.js +39 -0
  76. package/dist/core/atomic.js.map +1 -0
  77. package/dist/core/client.d.ts +238 -0
  78. package/dist/core/client.d.ts.map +1 -1
  79. package/dist/core/client.js +536 -4
  80. package/dist/core/client.js.map +1 -1
  81. package/dist/core/delegation.d.ts +184 -0
  82. package/dist/core/delegation.d.ts.map +1 -0
  83. package/dist/core/delegation.js +37 -0
  84. package/dist/core/delegation.js.map +1 -0
  85. package/dist/core/programmable.d.ts +66 -0
  86. package/dist/core/programmable.d.ts.map +1 -0
  87. package/dist/core/programmable.js +36 -0
  88. package/dist/core/programmable.js.map +1 -0
  89. package/dist/core/solvency.d.ts +223 -0
  90. package/dist/core/solvency.d.ts.map +1 -0
  91. package/dist/core/solvency.js +267 -0
  92. package/dist/core/solvency.js.map +1 -0
  93. package/dist/core/types.d.ts +172 -1
  94. package/dist/core/types.d.ts.map +1 -1
  95. package/dist/core/universal-account.d.ts +438 -0
  96. package/dist/core/universal-account.d.ts.map +1 -0
  97. package/dist/core/universal-account.js +597 -0
  98. package/dist/core/universal-account.js.map +1 -0
  99. package/dist/core/witness.d.ts +197 -0
  100. package/dist/core/witness.d.ts.map +1 -0
  101. package/dist/core/witness.js +298 -0
  102. package/dist/core/witness.js.map +1 -0
  103. package/dist/erc4337/types.js +2 -2
  104. package/dist/index.d.ts +30 -2
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +91 -4
  107. package/dist/index.js.map +1 -1
  108. package/dist/settlement/settlement.d.ts +152 -0
  109. package/dist/settlement/settlement.d.ts.map +1 -0
  110. package/dist/settlement/settlement.js +172 -0
  111. package/dist/settlement/settlement.js.map +1 -0
  112. package/dist/utils/eip712.js +2 -2
  113. package/dist/utils/fee.d.ts +2 -2
  114. package/dist/utils/fee.js +2 -2
  115. package/dist/utils/optional-deps.d.ts +9 -0
  116. package/dist/utils/optional-deps.d.ts.map +1 -0
  117. package/dist/utils/optional-deps.js +21 -0
  118. package/dist/utils/optional-deps.js.map +1 -0
  119. package/dist/utils/validation.d.ts.map +1 -1
  120. package/dist/utils/validation.js +6 -1
  121. package/dist/utils/validation.js.map +1 -1
  122. package/dist/wallet/wallet.d.ts +52 -0
  123. package/dist/wallet/wallet.d.ts.map +1 -1
  124. package/dist/wallet/wallet.js +265 -33
  125. package/dist/wallet/wallet.js.map +1 -1
  126. package/package.json +35 -1
@@ -0,0 +1,790 @@
1
+ "use strict";
2
+ /**
3
+ * Kaspa Chain Adapter
4
+ *
5
+ * Builds Kaspa UTXO transactions, computes per-input sighashes for
6
+ * BIP-340 Schnorr signing via the FROST threshold signing network,
7
+ * and broadcasts via the Kaspa REST API.
8
+ *
9
+ * Kaspa uses secp256k1 with BIP-340 Schnorr signatures (64 bytes),
10
+ * the same signature scheme as Bitcoin Taproot. FROST-secp256k1
11
+ * produces native BIP-340 compatible Schnorr signatures.
12
+ *
13
+ * Key features:
14
+ * - UTXO-based transaction model (similar to Bitcoin)
15
+ * - BIP-340 Schnorr signatures (64 bytes, same as Taproot)
16
+ * - ~1 second block time (BlockDAG architecture)
17
+ * - Per-input sighash computation for multi-input transactions
18
+ *
19
+ * No external dependencies — uses raw HTTP API calls and Node.js crypto.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { KaspaAdapter } from '@sequence0/sdk';
24
+ *
25
+ * const kaspa = new KaspaAdapter('mainnet');
26
+ *
27
+ * // Build a transfer transaction
28
+ * const unsignedTx = await kaspa.buildTransaction(
29
+ * { to: 'kaspa:qr...recipient', amount: 100000000 }, // 1 KAS
30
+ * 'kaspa:qr...sender'
31
+ * );
32
+ *
33
+ * // ... sign each sighash via FROST Schnorr ...
34
+ * const signedTx = await kaspa.attachSignature(unsignedTx, signatureHex);
35
+ * const txId = await kaspa.broadcast(signedTx);
36
+ * ```
37
+ */
38
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
39
+ if (k2 === undefined) k2 = k;
40
+ var desc = Object.getOwnPropertyDescriptor(m, k);
41
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
42
+ desc = { enumerable: true, get: function() { return m[k]; } };
43
+ }
44
+ Object.defineProperty(o, k2, desc);
45
+ }) : (function(o, m, k, k2) {
46
+ if (k2 === undefined) k2 = k;
47
+ o[k2] = m[k];
48
+ }));
49
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
50
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
51
+ }) : function(o, v) {
52
+ o["default"] = v;
53
+ });
54
+ var __importStar = (this && this.__importStar) || (function () {
55
+ var ownKeys = function(o) {
56
+ ownKeys = Object.getOwnPropertyNames || function (o) {
57
+ var ar = [];
58
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
+ return ar;
60
+ };
61
+ return ownKeys(o);
62
+ };
63
+ return function (mod) {
64
+ if (mod && mod.__esModule) return mod;
65
+ var result = {};
66
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
67
+ __setModuleDefault(result, mod);
68
+ return result;
69
+ };
70
+ })();
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.KaspaAdapter = void 0;
73
+ exports.createKaspaAdapter = createKaspaAdapter;
74
+ exports.createKaspaTestnetAdapter = createKaspaTestnetAdapter;
75
+ const errors_1 = require("../utils/errors");
76
+ const crypto = __importStar(require("crypto"));
77
+ // ── Network Configuration ──
78
+ const DEFAULT_API_URLS = {
79
+ 'mainnet': 'https://api.kaspa.org',
80
+ 'testnet': 'https://api-tn.kaspa.org',
81
+ };
82
+ /** Default fee rate in sompi per gram (mass unit) */
83
+ const DEFAULT_FEE_RATE = 1;
84
+ /** Minimum relay fee in sompi per gram */
85
+ const MIN_FEE_RATE = 1;
86
+ /** Dust threshold in sompi — outputs below this are rejected */
87
+ const DUST_THRESHOLD = 546;
88
+ /** Estimated mass per input (bytes) */
89
+ const MASS_PER_INPUT = 239;
90
+ /** Estimated mass per output (bytes) */
91
+ const MASS_PER_OUTPUT = 34;
92
+ /** Fixed transaction overhead mass */
93
+ const TX_OVERHEAD_MASS = 83;
94
+ /** Sompi per KAS (1 KAS = 1e8 sompi) */
95
+ const SOMPI_PER_KAS = 100000000;
96
+ // ── Address Encoding/Decoding ──
97
+ const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
98
+ /**
99
+ * Polymod for Kaspa's bech32-like address encoding
100
+ */
101
+ function polymod(values) {
102
+ const GEN = [
103
+ 0x98f2bc8e61n,
104
+ 0x79b76d99e2n,
105
+ 0xf33e5fb3c4n,
106
+ 0xae2eabe2a8n,
107
+ 0x1e4f43e470n,
108
+ ];
109
+ let chk = 1n;
110
+ for (const v of values) {
111
+ const b = chk >> 35n;
112
+ chk = ((chk & 0x07ffffffffn) << 5n) ^ BigInt(v);
113
+ for (let i = 0; i < 5; i++) {
114
+ if ((b >> BigInt(i)) & 1n) {
115
+ chk ^= GEN[i];
116
+ }
117
+ }
118
+ }
119
+ return chk ^ 1n;
120
+ }
121
+ /**
122
+ * Expand prefix for checksum computation
123
+ */
124
+ function prefixExpand(prefix) {
125
+ const ret = [];
126
+ for (const c of prefix) {
127
+ ret.push(c.charCodeAt(0) & 0x1f);
128
+ }
129
+ ret.push(0);
130
+ return ret;
131
+ }
132
+ /**
133
+ * Convert between bit widths
134
+ */
135
+ function convertBits(data, fromBits, toBits, pad) {
136
+ let acc = 0;
137
+ let bits = 0;
138
+ const ret = [];
139
+ const maxv = (1 << toBits) - 1;
140
+ for (const value of data) {
141
+ if (value < 0 || value >> fromBits !== 0) {
142
+ throw new Error('Invalid value for bit conversion');
143
+ }
144
+ acc = (acc << fromBits) | value;
145
+ bits += fromBits;
146
+ while (bits >= toBits) {
147
+ bits -= toBits;
148
+ ret.push((acc >> bits) & maxv);
149
+ }
150
+ }
151
+ if (pad) {
152
+ if (bits > 0) {
153
+ ret.push((acc << (toBits - bits)) & maxv);
154
+ }
155
+ }
156
+ else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
157
+ throw new Error('Invalid padding');
158
+ }
159
+ return ret;
160
+ }
161
+ /**
162
+ * Decode a kaspa:... address into its components.
163
+ *
164
+ * Kaspa addresses use a bech32-like encoding with format:
165
+ * kaspa:<type><payload><checksum>
166
+ *
167
+ * Type 0x00 = P2PK (pay-to-public-key, Schnorr)
168
+ * Type 0x01 = P2SH (pay-to-script-hash)
169
+ */
170
+ function decodeKaspaAddress(address) {
171
+ const parts = address.split(':');
172
+ if (parts.length !== 2) {
173
+ throw new Error(`Invalid Kaspa address format: ${address}`);
174
+ }
175
+ const prefix = parts[0];
176
+ if (prefix !== 'kaspa' && prefix !== 'kaspatest') {
177
+ throw new Error(`Invalid Kaspa address prefix: ${prefix}`);
178
+ }
179
+ const dataStr = parts[1];
180
+ const data5bit = [];
181
+ for (const c of dataStr) {
182
+ const idx = CHARSET.indexOf(c);
183
+ if (idx < 0)
184
+ throw new Error(`Invalid character in Kaspa address: ${c}`);
185
+ data5bit.push(idx);
186
+ }
187
+ // Verify checksum (last 8 characters = 40 bits)
188
+ const checksumData = prefixExpand(prefix).concat(data5bit);
189
+ const mod = polymod(checksumData);
190
+ if (mod !== 0n) {
191
+ throw new Error('Invalid Kaspa address checksum');
192
+ }
193
+ // Remove checksum (8 characters)
194
+ const payload5bit = data5bit.slice(0, data5bit.length - 8);
195
+ // Convert from 5-bit to 8-bit
196
+ const payload8bit = convertBits(payload5bit, 5, 8, false);
197
+ // First byte is the address type
198
+ const type = payload8bit[0];
199
+ const payloadBytes = Buffer.from(payload8bit.slice(1));
200
+ return { prefix, type, payload: payloadBytes };
201
+ }
202
+ /**
203
+ * Encode a public key as a kaspa:... address
204
+ *
205
+ * @param prefix - Network prefix ('kaspa' or 'kaspatest')
206
+ * @param type - Address type (0x00 for P2PK Schnorr)
207
+ * @param payload - 32-byte public key or script hash
208
+ */
209
+ function encodeKaspaAddress(prefix, type, payload) {
210
+ // Prepend type byte to payload
211
+ const fullPayload = Buffer.concat([Buffer.from([type]), payload]);
212
+ // Convert to 5-bit groups
213
+ const data5bit = convertBits(Array.from(fullPayload), 8, 5, true);
214
+ // Compute checksum
215
+ const checksumInput = prefixExpand(prefix).concat(data5bit).concat([0, 0, 0, 0, 0, 0, 0, 0]);
216
+ const mod = polymod(checksumInput);
217
+ const checksum = [];
218
+ for (let i = 0; i < 8; i++) {
219
+ checksum.push(Number((mod >> BigInt(5 * (7 - i))) & 0x1fn));
220
+ }
221
+ let result = prefix + ':';
222
+ for (const d of data5bit.concat(checksum)) {
223
+ result += CHARSET[d];
224
+ }
225
+ return result;
226
+ }
227
+ /**
228
+ * Build a P2PK ScriptPublicKey for a Schnorr public key.
229
+ *
230
+ * Kaspa P2PK Schnorr script: OP_DATA_32 <32-byte-pubkey> OP_CHECKSIG
231
+ * Hex: 20 <pubkey_hex> ac
232
+ */
233
+ function buildP2pkScript(pubkeyHex) {
234
+ return '20' + pubkeyHex + 'ac';
235
+ }
236
+ // ── Sighash Computation ──
237
+ /**
238
+ * Compute a tagged hash: SHA-256(SHA-256(tag) || SHA-256(tag) || data)
239
+ *
240
+ * Kaspa uses tagged hashes similar to BIP-340/BIP-341 for domain separation.
241
+ */
242
+ function taggedHash(tag, ...data) {
243
+ const tagHash = crypto.createHash('sha256').update(tag).digest();
244
+ const hasher = crypto.createHash('sha256');
245
+ hasher.update(tagHash);
246
+ hasher.update(tagHash);
247
+ for (const d of data) {
248
+ hasher.update(d);
249
+ }
250
+ return hasher.digest();
251
+ }
252
+ /**
253
+ * Write a 64-bit unsigned integer in little-endian format
254
+ */
255
+ function writeUint64LE(value) {
256
+ const buf = Buffer.alloc(8);
257
+ buf.writeBigUInt64LE(BigInt(value));
258
+ return buf;
259
+ }
260
+ /**
261
+ * Write a 32-bit unsigned integer in little-endian format
262
+ */
263
+ function writeUint32LE(value) {
264
+ const buf = Buffer.alloc(4);
265
+ buf.writeUInt32LE(value);
266
+ return buf;
267
+ }
268
+ /**
269
+ * Write a 16-bit unsigned integer in little-endian format
270
+ */
271
+ function writeUint16LE(value) {
272
+ const buf = Buffer.alloc(2);
273
+ buf.writeUInt16LE(value);
274
+ return buf;
275
+ }
276
+ /**
277
+ * Compute the sighash for a Kaspa transaction input.
278
+ *
279
+ * Kaspa uses a sighash scheme inspired by BIP-341 (Taproot) with
280
+ * domain-separated tagged hashes. Each input has its own sighash
281
+ * that must be independently signed with Schnorr.
282
+ *
283
+ * The sighash commits to:
284
+ * - Transaction version and subnetwork
285
+ * - Hash of all previous outputs (outpoints)
286
+ * - Hash of all sequence numbers
287
+ * - Hash of all outputs
288
+ * - The specific input being signed (outpoint, script, value, sequence)
289
+ * - Sighash type
290
+ */
291
+ function computeKaspaSighash(inputs, outputs, inputIndex) {
292
+ // Hash all previous outpoints
293
+ // Kaspa uses blake2b internally, but we use SHA-256 for sighash computation
294
+ // since the FROST signing protocol operates on the final sighash digest
295
+ const hashPrevOuts = (() => {
296
+ const hasher = crypto.createHash('sha256');
297
+ for (const inp of inputs) {
298
+ // Transaction ID (32 bytes, already big-endian in Kaspa)
299
+ hasher.update(Buffer.from(inp.transactionId, 'hex'));
300
+ // Output index (4 bytes LE)
301
+ hasher.update(writeUint32LE(inp.index));
302
+ }
303
+ return hasher.digest();
304
+ })();
305
+ // Hash all sequence numbers
306
+ const hashSequences = (() => {
307
+ const hasher = crypto.createHash('sha256');
308
+ for (let i = 0; i < inputs.length; i++) {
309
+ // All inputs use max sequence (0xFFFFFFFFFFFFFFFF for Kaspa's u64 sequence)
310
+ hasher.update(Buffer.from('ffffffffffffffff', 'hex'));
311
+ }
312
+ return hasher.digest();
313
+ })();
314
+ // Hash sig op counts
315
+ const hashSigOpCounts = (() => {
316
+ const hasher = crypto.createHash('sha256');
317
+ for (let i = 0; i < inputs.length; i++) {
318
+ // 1 sig op per P2PK input
319
+ hasher.update(Buffer.from([1]));
320
+ }
321
+ return hasher.digest();
322
+ })();
323
+ // Hash all outputs
324
+ const hashOutputs = (() => {
325
+ const hasher = crypto.createHash('sha256');
326
+ for (const out of outputs) {
327
+ // Value (8 bytes LE)
328
+ hasher.update(writeUint64LE(out.value));
329
+ // Script version (2 bytes LE)
330
+ hasher.update(writeUint16LE(0));
331
+ // Script length (8 bytes LE for Kaspa's u64 length prefix)
332
+ const scriptBuf = Buffer.from(out.scriptPublicKey, 'hex');
333
+ hasher.update(writeUint64LE(scriptBuf.length));
334
+ // Script
335
+ hasher.update(scriptBuf);
336
+ }
337
+ return hasher.digest();
338
+ })();
339
+ // Build the sighash preimage
340
+ const preimage = Buffer.concat([
341
+ // Version (2 bytes LE)
342
+ writeUint16LE(0),
343
+ // Hash previous outputs
344
+ hashPrevOuts,
345
+ // Hash sequences
346
+ hashSequences,
347
+ // Hash sig op counts
348
+ hashSigOpCounts,
349
+ // Current input's outpoint
350
+ Buffer.from(inputs[inputIndex].transactionId, 'hex'),
351
+ writeUint32LE(inputs[inputIndex].index),
352
+ // Current input's script version (2 bytes LE)
353
+ writeUint16LE(0),
354
+ // Current input's script length (8 bytes LE)
355
+ (() => {
356
+ const scriptBuf = Buffer.from(inputs[inputIndex].scriptPublicKey, 'hex');
357
+ return Buffer.concat([writeUint64LE(scriptBuf.length), scriptBuf]);
358
+ })(),
359
+ // Current input's value (8 bytes LE)
360
+ writeUint64LE(inputs[inputIndex].amount),
361
+ // Current input's sequence (8 bytes LE)
362
+ Buffer.from('ffffffffffffffff', 'hex'),
363
+ // Current input's sig op count
364
+ Buffer.from([1]),
365
+ // Hash outputs
366
+ hashOutputs,
367
+ // Lock time (8 bytes LE)
368
+ writeUint64LE(0),
369
+ // Subnetwork ID (20 bytes, all zeros for native)
370
+ Buffer.alloc(20, 0),
371
+ // Gas (8 bytes LE)
372
+ writeUint64LE(0),
373
+ // Payload hash (32 bytes, all zeros for no payload)
374
+ Buffer.alloc(32, 0),
375
+ // Sighash type (4 bytes LE) — SigHashAll = 0x01
376
+ writeUint32LE(1),
377
+ ]);
378
+ // Double SHA-256 of the preimage (Kaspa sighash)
379
+ const first = crypto.createHash('sha256').update(preimage).digest();
380
+ const sighash = crypto.createHash('sha256').update(first).digest();
381
+ return sighash.toString('hex');
382
+ }
383
+ // ── Adapter ──
384
+ class KaspaAdapter {
385
+ /**
386
+ * Create a Kaspa adapter.
387
+ *
388
+ * @param network - Network to connect to: 'mainnet' or 'testnet'
389
+ * @param apiUrl - Custom API URL (overrides the default for the network)
390
+ */
391
+ constructor(network = 'mainnet', apiUrl) {
392
+ this.network = network;
393
+ this.apiUrl = apiUrl || DEFAULT_API_URLS[network];
394
+ }
395
+ getRpcUrl() {
396
+ return this.apiUrl;
397
+ }
398
+ // ────────────────────────────────────────────────
399
+ // Build Transaction
400
+ // ────────────────────────────────────────────────
401
+ /**
402
+ * Build an unsigned Kaspa transaction.
403
+ *
404
+ * Fetches UTXOs for the sender, selects inputs to cover the amount
405
+ * plus fees, builds outputs (payment + change), and computes
406
+ * per-input sighashes for Schnorr signing.
407
+ *
408
+ * Returns hex-encoded JSON containing the TX data and sighashes.
409
+ * Each sighash must be independently signed via FROST Schnorr.
410
+ *
411
+ * @param tx - Transaction parameters (to, amount, feeRate)
412
+ * @param fromAddress - Sender kaspa:... address
413
+ * @returns Hex-encoded payload with sighashes and TX structure
414
+ */
415
+ async buildTransaction(tx, fromAddress) {
416
+ try {
417
+ // Validate addresses
418
+ this.validateAddress(fromAddress);
419
+ this.validateAddress(tx.to);
420
+ // Fetch UTXOs for the sender
421
+ const utxos = await this.fetchUtxos(fromAddress);
422
+ if (utxos.length === 0) {
423
+ throw new Error(`No UTXOs found for ${fromAddress}`);
424
+ }
425
+ const feeRate = tx.feeRate || DEFAULT_FEE_RATE;
426
+ // Sort UTXOs by value (largest first) for efficient selection
427
+ const sorted = [...utxos].sort((a, b) => b.amount - a.amount);
428
+ // Select inputs
429
+ const selectedInputs = [];
430
+ let inputTotal = 0;
431
+ for (const utxo of sorted) {
432
+ selectedInputs.push(utxo);
433
+ inputTotal += utxo.amount;
434
+ // Estimate transaction mass (fee = mass * feeRate)
435
+ const estimatedMass = TX_OVERHEAD_MASS +
436
+ selectedInputs.length * MASS_PER_INPUT +
437
+ 2 * MASS_PER_OUTPUT; // payment + change
438
+ const estimatedFee = estimatedMass * feeRate;
439
+ if (inputTotal >= tx.amount + estimatedFee)
440
+ break;
441
+ }
442
+ // Calculate actual fee
443
+ const numOutputs = 2; // payment + change (may drop to 1 if change < dust)
444
+ const txMass = TX_OVERHEAD_MASS +
445
+ selectedInputs.length * MASS_PER_INPUT +
446
+ numOutputs * MASS_PER_OUTPUT;
447
+ const fee = Math.max(txMass * feeRate, MIN_FEE_RATE);
448
+ if (inputTotal < tx.amount + fee) {
449
+ throw new Error(`Insufficient funds: have ${inputTotal} sompi, need ${tx.amount + fee} sompi ` +
450
+ `(${tx.amount} amount + ${fee} fee)`);
451
+ }
452
+ const change = inputTotal - tx.amount - fee;
453
+ // Decode the recipient address to get the scriptPublicKey
454
+ const recipientAddr = decodeKaspaAddress(tx.to);
455
+ const recipientScript = buildP2pkScript(recipientAddr.payload.toString('hex'));
456
+ // Build outputs
457
+ const outputs = [
458
+ {
459
+ address: tx.to,
460
+ value: tx.amount,
461
+ scriptPublicKey: recipientScript,
462
+ },
463
+ ];
464
+ // Add change output if above dust threshold
465
+ if (change > DUST_THRESHOLD) {
466
+ const senderAddr = decodeKaspaAddress(fromAddress);
467
+ const senderScript = buildP2pkScript(senderAddr.payload.toString('hex'));
468
+ outputs.push({
469
+ address: fromAddress,
470
+ value: change,
471
+ scriptPublicKey: senderScript,
472
+ });
473
+ }
474
+ // Build input descriptors for sighash computation
475
+ const inputDescs = selectedInputs.map((utxo) => ({
476
+ transactionId: utxo.transactionId,
477
+ index: utxo.index,
478
+ amount: utxo.amount,
479
+ scriptPublicKey: utxo.scriptPublicKey.scriptPublicKey,
480
+ }));
481
+ const outputDescs = outputs.map((out) => ({
482
+ value: out.value,
483
+ scriptPublicKey: out.scriptPublicKey,
484
+ }));
485
+ // Compute per-input sighashes
486
+ const sighashes = [];
487
+ for (let i = 0; i < inputDescs.length; i++) {
488
+ const sighash = computeKaspaSighash(inputDescs, outputDescs, i);
489
+ sighashes.push(sighash);
490
+ }
491
+ const payload = {
492
+ sighash: sighashes[0], // Primary sighash (for single-input convenience)
493
+ sighashes,
494
+ inputs: inputDescs,
495
+ outputs: outputs.map((out) => ({
496
+ address: out.address,
497
+ value: out.value,
498
+ scriptPublicKey: out.scriptPublicKey,
499
+ })),
500
+ fee,
501
+ mass: txMass,
502
+ network: this.network,
503
+ };
504
+ return Buffer.from(JSON.stringify(payload)).toString('hex');
505
+ }
506
+ catch (e) {
507
+ if (e instanceof errors_1.ChainError)
508
+ throw e;
509
+ throw new errors_1.ChainError(`Failed to build Kaspa transaction: ${e.message}`, 'kaspa');
510
+ }
511
+ }
512
+ /**
513
+ * Extract the signing payload from the buildTransaction output.
514
+ *
515
+ * Returns the primary sighash (first input). For multi-input
516
+ * transactions, callers should decode the full payload to get
517
+ * all sighashes from the `sighashes` array.
518
+ */
519
+ getSigningPayload(unsignedTx) {
520
+ try {
521
+ const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
522
+ return payload.sighash;
523
+ }
524
+ catch {
525
+ return unsignedTx;
526
+ }
527
+ }
528
+ // ────────────────────────────────────────────────
529
+ // Attach Signature
530
+ // ────────────────────────────────────────────────
531
+ /**
532
+ * Attach BIP-340 Schnorr signature(s) to the Kaspa transaction.
533
+ *
534
+ * Each input requires its own 64-byte Schnorr signature. For
535
+ * multi-input transactions, pass comma-separated signatures or
536
+ * concatenated 64-byte signatures.
537
+ *
538
+ * @param unsignedTx - Hex-encoded unsigned transaction from buildTransaction
539
+ * @param signature - 64-byte Schnorr signature(s) (hex-encoded)
540
+ * @returns Hex-encoded signed transaction payload
541
+ */
542
+ async attachSignature(unsignedTx, signature) {
543
+ try {
544
+ const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
545
+ const sig = signature.startsWith('0x') ? signature.slice(2) : signature;
546
+ // Parse signatures — support comma-separated or concatenated
547
+ let signatures;
548
+ if (sig.includes(',')) {
549
+ signatures = sig.split(',').map((s) => s.trim());
550
+ }
551
+ else if (sig.length > 128 && payload.inputs.length > 1) {
552
+ // Concatenated 64-byte (128 hex char) signatures
553
+ signatures = [];
554
+ for (let i = 0; i < payload.inputs.length; i++) {
555
+ signatures.push(sig.slice(i * 128, (i + 1) * 128));
556
+ }
557
+ }
558
+ else {
559
+ // Single signature — reuse for all inputs (common for same-key UTXOs)
560
+ signatures = [sig];
561
+ }
562
+ // Validate signature lengths
563
+ for (let i = 0; i < signatures.length; i++) {
564
+ if (signatures[i].length !== 128) {
565
+ throw new Error(`Invalid Schnorr signature length for input ${i}: ` +
566
+ `expected 128 hex chars (64 bytes), got ${signatures[i].length}`);
567
+ }
568
+ }
569
+ const signedPayload = {
570
+ ...payload,
571
+ signatures,
572
+ };
573
+ return Buffer.from(JSON.stringify(signedPayload)).toString('hex');
574
+ }
575
+ catch (e) {
576
+ if (e instanceof errors_1.ChainError)
577
+ throw e;
578
+ throw new errors_1.ChainError(`Failed to attach Kaspa signature: ${e.message}`, 'kaspa');
579
+ }
580
+ }
581
+ // ────────────────────────────────────────────────
582
+ // Broadcast
583
+ // ────────────────────────────────────────────────
584
+ /**
585
+ * Broadcast a signed Kaspa transaction.
586
+ *
587
+ * Constructs the RPC-compatible transaction object with inputs,
588
+ * outputs, and witness signatures, then sends it to the Kaspa
589
+ * REST API for propagation to the network.
590
+ *
591
+ * @param signedTx - Hex-encoded signed transaction from attachSignature
592
+ * @returns Transaction ID
593
+ */
594
+ async broadcast(signedTx) {
595
+ try {
596
+ const payload = JSON.parse(Buffer.from(signedTx, 'hex').toString());
597
+ if (!payload.signatures || payload.signatures.length === 0) {
598
+ throw new Error('Transaction is not signed (missing signatures)');
599
+ }
600
+ // Build the transaction object in Kaspa RPC format
601
+ const rpcInputs = payload.inputs.map((inp, i) => {
602
+ const sigIdx = Math.min(i, payload.signatures.length - 1);
603
+ return {
604
+ previousOutpoint: {
605
+ transactionId: inp.transactionId,
606
+ index: inp.index,
607
+ },
608
+ signatureScript: payload.signatures[sigIdx], // 64-byte Schnorr sig hex
609
+ sequence: '18446744073709551615', // u64 max as string
610
+ sigOpCount: 1,
611
+ };
612
+ });
613
+ const rpcOutputs = payload.outputs.map((out) => ({
614
+ amount: out.value,
615
+ scriptPublicKey: {
616
+ scriptPublicKey: out.scriptPublicKey,
617
+ version: 0,
618
+ },
619
+ }));
620
+ const rpcTransaction = {
621
+ transaction: {
622
+ version: 0,
623
+ inputs: rpcInputs,
624
+ outputs: rpcOutputs,
625
+ lockTime: '0',
626
+ subnetworkId: '0000000000000000000000000000000000000000',
627
+ },
628
+ };
629
+ const response = await fetch(`${this.apiUrl}/transactions`, {
630
+ method: 'POST',
631
+ headers: {
632
+ 'Content-Type': 'application/json',
633
+ },
634
+ body: JSON.stringify(rpcTransaction),
635
+ });
636
+ if (!response.ok) {
637
+ const errorText = await response.text();
638
+ throw new Error(`Broadcast failed (HTTP ${response.status}): ${errorText}`);
639
+ }
640
+ const result = await response.json();
641
+ // The API returns the transaction ID
642
+ return result.transactionId || result.txId || '';
643
+ }
644
+ catch (e) {
645
+ if (e instanceof errors_1.ChainError)
646
+ throw e;
647
+ throw new errors_1.ChainError(`Failed to broadcast Kaspa tx: ${e.message}`, 'kaspa');
648
+ }
649
+ }
650
+ // ────────────────────────────────────────────────
651
+ // Balance
652
+ // ────────────────────────────────────────────────
653
+ /**
654
+ * Get the KAS balance for an address.
655
+ *
656
+ * Returns balance in sompi (1 KAS = 1e8 sompi).
657
+ *
658
+ * @param address - Kaspa kaspa:... address
659
+ * @returns Balance in sompi as a string
660
+ */
661
+ async getBalance(address) {
662
+ try {
663
+ this.validateAddress(address);
664
+ const response = await fetch(`${this.apiUrl}/addresses/${address}/balance`);
665
+ if (!response.ok) {
666
+ if (response.status === 404)
667
+ return '0';
668
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
669
+ }
670
+ const data = await response.json();
671
+ return (data.balance ?? data.address?.balance ?? 0).toString();
672
+ }
673
+ catch (e) {
674
+ if (e instanceof errors_1.ChainError)
675
+ throw e;
676
+ // Return '0' for network errors (address may have no history)
677
+ return '0';
678
+ }
679
+ }
680
+ // ────────────────────────────────────────────────
681
+ // Utility Methods
682
+ // ────────────────────────────────────────────────
683
+ /**
684
+ * Fetch UTXOs for a Kaspa address from the REST API.
685
+ *
686
+ * @param address - kaspa:... address
687
+ * @returns Array of UTXOs
688
+ */
689
+ async fetchUtxos(address) {
690
+ try {
691
+ const response = await fetch(`${this.apiUrl}/addresses/${address}/utxos`);
692
+ if (!response.ok) {
693
+ if (response.status === 404)
694
+ return [];
695
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
696
+ }
697
+ const data = await response.json();
698
+ return data.map((item) => ({
699
+ transactionId: item.outpoint?.transactionId || item.transactionId,
700
+ index: item.outpoint?.index ?? item.index ?? 0,
701
+ amount: parseInt(item.utxoEntry?.amount ?? item.amount ?? '0', 10),
702
+ scriptPublicKey: item.utxoEntry?.scriptPublicKey || item.scriptPublicKey || {
703
+ scriptPublicKey: '',
704
+ version: 0,
705
+ },
706
+ isCoinbase: item.utxoEntry?.isCoinbase ?? item.isCoinbase ?? false,
707
+ blockDaaScore: item.utxoEntry?.blockDaaScore ?? item.blockDaaScore ?? '0',
708
+ }));
709
+ }
710
+ catch (e) {
711
+ if (e instanceof errors_1.ChainError)
712
+ throw e;
713
+ throw new errors_1.ChainError(`Failed to fetch Kaspa UTXOs: ${e.message}`, 'kaspa');
714
+ }
715
+ }
716
+ /**
717
+ * Validate a Kaspa address format.
718
+ *
719
+ * @param address - Address to validate
720
+ * @throws Error if the address is invalid
721
+ */
722
+ validateAddress(address) {
723
+ const expectedPrefix = this.network === 'mainnet' ? 'kaspa' : 'kaspatest';
724
+ if (!address.startsWith(expectedPrefix + ':')) {
725
+ throw new Error(`Invalid Kaspa address for ${this.network}: expected prefix '${expectedPrefix}:', got '${address}'`);
726
+ }
727
+ // Will throw if the address is malformed
728
+ decodeKaspaAddress(address);
729
+ }
730
+ /**
731
+ * Derive a kaspa:... address from a 32-byte x-only public key.
732
+ *
733
+ * FROST-secp256k1 produces a group verifying key (compressed point).
734
+ * Extract the x-only (32-byte) key and encode as a Kaspa P2PK Schnorr address.
735
+ *
736
+ * @param pubkeyHex - 32-byte x-only public key (hex-encoded), or 33-byte compressed
737
+ * @returns kaspa:... address
738
+ */
739
+ deriveAddress(pubkeyHex) {
740
+ const clean = pubkeyHex.startsWith('0x') ? pubkeyHex.slice(2) : pubkeyHex;
741
+ let xOnlyHex;
742
+ if (clean.length === 66) {
743
+ // 33-byte compressed public key — strip the prefix byte
744
+ xOnlyHex = clean.slice(2);
745
+ }
746
+ else if (clean.length === 64) {
747
+ // Already 32-byte x-only
748
+ xOnlyHex = clean;
749
+ }
750
+ else {
751
+ throw new errors_1.ChainError(`Invalid public key length: expected 32 or 33 bytes, got ${clean.length / 2}`, 'kaspa');
752
+ }
753
+ const prefix = this.network === 'mainnet' ? 'kaspa' : 'kaspatest';
754
+ const payload = Buffer.from(xOnlyHex, 'hex');
755
+ return encodeKaspaAddress(prefix, 0x00, payload);
756
+ }
757
+ /**
758
+ * Estimate the fee for a transaction.
759
+ *
760
+ * @param numInputs - Number of inputs
761
+ * @param numOutputs - Number of outputs
762
+ * @param feeRate - Fee rate in sompi per gram (default: 1)
763
+ * @returns Estimated fee in sompi
764
+ */
765
+ estimateFee(numInputs, numOutputs, feeRate = DEFAULT_FEE_RATE) {
766
+ const mass = TX_OVERHEAD_MASS + numInputs * MASS_PER_INPUT + numOutputs * MASS_PER_OUTPUT;
767
+ return Math.max(mass * feeRate, MIN_FEE_RATE);
768
+ }
769
+ }
770
+ exports.KaspaAdapter = KaspaAdapter;
771
+ // ── Factory Functions ──
772
+ /**
773
+ * Create a Kaspa mainnet adapter
774
+ *
775
+ * @param apiUrl - Optional custom API URL
776
+ * @returns KaspaAdapter configured for mainnet
777
+ */
778
+ function createKaspaAdapter(apiUrl) {
779
+ return new KaspaAdapter('mainnet', apiUrl);
780
+ }
781
+ /**
782
+ * Create a Kaspa testnet adapter
783
+ *
784
+ * @param apiUrl - Optional custom API URL
785
+ * @returns KaspaAdapter configured for testnet
786
+ */
787
+ function createKaspaTestnetAdapter(apiUrl) {
788
+ return new KaspaAdapter('testnet', apiUrl);
789
+ }
790
+ //# sourceMappingURL=kaspa.js.map