@sequence0/sdk 1.1.0 → 1.2.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.
- package/README.md +1 -1
- package/dist/chains/algorand.d.ts +44 -0
- package/dist/chains/algorand.d.ts.map +1 -0
- package/dist/chains/algorand.js +148 -0
- package/dist/chains/algorand.js.map +1 -0
- package/dist/chains/aptos.d.ts +39 -0
- package/dist/chains/aptos.d.ts.map +1 -0
- package/dist/chains/aptos.js +168 -0
- package/dist/chains/aptos.js.map +1 -0
- package/dist/chains/bitcoin-taproot.d.ts +77 -14
- package/dist/chains/bitcoin-taproot.d.ts.map +1 -1
- package/dist/chains/bitcoin-taproot.js +324 -65
- package/dist/chains/bitcoin-taproot.js.map +1 -1
- package/dist/chains/bitcoin.d.ts +12 -7
- package/dist/chains/bitcoin.d.ts.map +1 -1
- package/dist/chains/bitcoin.js +14 -9
- package/dist/chains/bitcoin.js.map +1 -1
- package/dist/chains/cardano.d.ts +42 -0
- package/dist/chains/cardano.d.ts.map +1 -0
- package/dist/chains/cardano.js +188 -0
- package/dist/chains/cardano.js.map +1 -0
- package/dist/chains/cosmos.d.ts +42 -0
- package/dist/chains/cosmos.d.ts.map +1 -0
- package/dist/chains/cosmos.js +216 -0
- package/dist/chains/cosmos.js.map +1 -0
- package/dist/chains/dogecoin-litecoin.d.ts +57 -0
- package/dist/chains/dogecoin-litecoin.d.ts.map +1 -0
- package/dist/chains/dogecoin-litecoin.js +521 -0
- package/dist/chains/dogecoin-litecoin.js.map +1 -0
- package/dist/chains/ethereum.d.ts.map +1 -1
- package/dist/chains/ethereum.js +16 -0
- package/dist/chains/ethereum.js.map +1 -1
- package/dist/chains/hedera.d.ts +113 -0
- package/dist/chains/hedera.d.ts.map +1 -0
- package/dist/chains/hedera.js +302 -0
- package/dist/chains/hedera.js.map +1 -0
- package/dist/chains/icp.d.ts +95 -0
- package/dist/chains/icp.d.ts.map +1 -0
- package/dist/chains/icp.js +520 -0
- package/dist/chains/icp.js.map +1 -0
- package/dist/chains/kaspa.d.ts +152 -0
- package/dist/chains/kaspa.d.ts.map +1 -0
- package/dist/chains/kaspa.js +790 -0
- package/dist/chains/kaspa.js.map +1 -0
- package/dist/chains/multiversx.d.ts +143 -0
- package/dist/chains/multiversx.d.ts.map +1 -0
- package/dist/chains/multiversx.js +524 -0
- package/dist/chains/multiversx.js.map +1 -0
- package/dist/chains/near.d.ts +40 -0
- package/dist/chains/near.d.ts.map +1 -0
- package/dist/chains/near.js +170 -0
- package/dist/chains/near.js.map +1 -0
- package/dist/chains/polkadot.d.ts +43 -0
- package/dist/chains/polkadot.d.ts.map +1 -0
- package/dist/chains/polkadot.js +179 -0
- package/dist/chains/polkadot.js.map +1 -0
- package/dist/chains/ripple.d.ts +41 -0
- package/dist/chains/ripple.d.ts.map +1 -0
- package/dist/chains/ripple.js +190 -0
- package/dist/chains/ripple.js.map +1 -0
- package/dist/chains/stellar.d.ts +40 -0
- package/dist/chains/stellar.d.ts.map +1 -0
- package/dist/chains/stellar.js +156 -0
- package/dist/chains/stellar.js.map +1 -0
- package/dist/chains/sui.d.ts +44 -0
- package/dist/chains/sui.d.ts.map +1 -0
- package/dist/chains/sui.js +157 -0
- package/dist/chains/sui.js.map +1 -0
- package/dist/chains/tezos.d.ts +43 -0
- package/dist/chains/tezos.d.ts.map +1 -0
- package/dist/chains/tezos.js +162 -0
- package/dist/chains/tezos.js.map +1 -0
- package/dist/chains/ton.d.ts +40 -0
- package/dist/chains/ton.d.ts.map +1 -0
- package/dist/chains/ton.js +168 -0
- package/dist/chains/ton.js.map +1 -0
- package/dist/chains/tron.d.ts +41 -0
- package/dist/chains/tron.d.ts.map +1 -0
- package/dist/chains/tron.js +124 -0
- package/dist/chains/tron.js.map +1 -0
- package/dist/core/client.d.ts +4 -5
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +54 -29
- package/dist/core/client.js.map +1 -1
- package/dist/core/types.d.ts +161 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/erc4337/types.js +2 -2
- package/dist/index.d.ts +21 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -4
- package/dist/index.js.map +1 -1
- package/dist/utils/discovery.d.ts.map +1 -1
- package/dist/utils/discovery.js +56 -1
- package/dist/utils/discovery.js.map +1 -1
- package/dist/utils/eip712.d.ts +36 -0
- package/dist/utils/eip712.d.ts.map +1 -0
- package/dist/utils/eip712.js +80 -0
- package/dist/utils/eip712.js.map +1 -0
- package/dist/utils/fee.d.ts +2 -2
- package/dist/utils/fee.js +2 -2
- package/dist/utils/optional-deps.d.ts +9 -0
- package/dist/utils/optional-deps.d.ts.map +1 -0
- package/dist/utils/optional-deps.js +21 -0
- package/dist/utils/optional-deps.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +24 -1
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/websocket.js +1 -1
- package/dist/utils/websocket.js.map +1 -1
- package/dist/wallet/wallet.d.ts +16 -2
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +131 -58
- package/dist/wallet/wallet.js.map +1 -1
- 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
|