@xchainjs/xchain-monero 0.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/lib/index.js ADDED
@@ -0,0 +1,2538 @@
1
+ 'use strict';
2
+
3
+ var xchainClient = require('@xchainjs/xchain-client');
4
+ var xchainCrypto = require('@xchainjs/xchain-crypto');
5
+ var xchainUtil = require('@xchainjs/xchain-util');
6
+ var sha3 = require('@noble/hashes/sha3');
7
+ var slip10 = require('micro-key-producer/slip10.js');
8
+ var ed25519 = require('@noble/curves/ed25519');
9
+ var utils = require('@noble/curves/abstract/utils');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var slip10__default = /*#__PURE__*/_interopDefault(slip10);
14
+
15
+ /******************************************************************************
16
+ Copyright (c) Microsoft Corporation.
17
+
18
+ Permission to use, copy, modify, and/or distribute this software for any
19
+ purpose with or without fee is hereby granted.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
22
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
23
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
24
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
25
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
26
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
27
+ PERFORMANCE OF THIS SOFTWARE.
28
+ ***************************************************************************** */
29
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
30
+
31
+
32
+ function __awaiter(thisArg, _arguments, P, generator) {
33
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
34
+ return new (P || (P = Promise))(function (resolve, reject) {
35
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
36
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
37
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
38
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
39
+ });
40
+ }
41
+
42
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
43
+ var e = new Error(message);
44
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
45
+ };
46
+
47
+ /**
48
+ * Monero chain symbol
49
+ */
50
+ const XMRChain = 'XMR';
51
+ /**
52
+ * Monero native asset decimals (piconero = 10^-12 XMR)
53
+ */
54
+ const XMR_DECIMALS = 12;
55
+ /**
56
+ * Monero native asset
57
+ */
58
+ const AssetXMR = {
59
+ chain: 'XMR',
60
+ ticker: 'XMR',
61
+ symbol: 'XMR',
62
+ type: xchainUtil.AssetType.NATIVE,
63
+ };
64
+ const mainnetExplorer = new xchainClient.ExplorerProvider('https://xmrchain.net/', 'https://xmrchain.net/search?value=%%ADDRESS%%', 'https://xmrchain.net/tx/%%TX_ID%%');
65
+ const defaultXMRParams = {
66
+ network: xchainClient.Network.Mainnet,
67
+ rootDerivationPaths: {
68
+ [xchainClient.Network.Mainnet]: "m/44'/128'/",
69
+ [xchainClient.Network.Testnet]: "m/44'/128'/",
70
+ [xchainClient.Network.Stagenet]: "m/44'/128'/",
71
+ },
72
+ explorerProviders: {
73
+ [xchainClient.Network.Mainnet]: mainnetExplorer,
74
+ [xchainClient.Network.Testnet]: new xchainClient.ExplorerProvider('https://stagenet.xmrchain.net/', 'https://stagenet.xmrchain.net/search?value=%%ADDRESS%%', 'https://stagenet.xmrchain.net/tx/%%TX_ID%%'),
75
+ [xchainClient.Network.Stagenet]: mainnetExplorer,
76
+ },
77
+ daemonUrls: {
78
+ [xchainClient.Network.Mainnet]: ['https://xmr-node.cakewallet.com:18081', 'https://node.sethforprivacy.com'],
79
+ [xchainClient.Network.Testnet]: ['http://stagenet.xmr-tw.org:38081'],
80
+ [xchainClient.Network.Stagenet]: ['https://xmr-node.cakewallet.com:18081', 'https://node.sethforprivacy.com'],
81
+ },
82
+ lwsUrls: {
83
+ [xchainClient.Network.Mainnet]: [],
84
+ [xchainClient.Network.Testnet]: [],
85
+ [xchainClient.Network.Stagenet]: [],
86
+ },
87
+ };
88
+
89
+ /**
90
+ * Monero's custom base58 encoding.
91
+ * Unlike Bitcoin's base58, Monero encodes in fixed 8-byte blocks → 11-char blocks.
92
+ * The last block may be shorter depending on remaining bytes.
93
+ */
94
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
95
+ /** Number of base58 chars needed to encode N bytes (for N = 0..8) */
96
+ const ENCODED_BLOCK_SIZES = [0, 2, 3, 5, 6, 7, 9, 10, 11];
97
+ const FULL_BLOCK_SIZE = 8;
98
+ const FULL_ENCODED_BLOCK_SIZE = 11;
99
+ /**
100
+ * Encodes a block of bytes (up to 8) into base58 characters.
101
+ */
102
+ function encodeBlock(data) {
103
+ if (data.length === 0)
104
+ return '';
105
+ const size = ENCODED_BLOCK_SIZES[data.length];
106
+ // Convert bytes to a big number (big-endian)
107
+ let num = BigInt(0);
108
+ for (let i = 0; i < data.length; i++) {
109
+ num = (num << BigInt(8)) | BigInt(data[i]);
110
+ }
111
+ // Convert to base58 with fixed-width output
112
+ const chars = new Array(size);
113
+ for (let i = size - 1; i >= 0; i--) {
114
+ chars[i] = ALPHABET[Number(num % BigInt(58))];
115
+ num = num / BigInt(58);
116
+ }
117
+ return chars.join('');
118
+ }
119
+ /**
120
+ * Decodes a base58-encoded block back to bytes.
121
+ */
122
+ function decodeBlock(encoded, targetLen) {
123
+ if (encoded.length === 0)
124
+ return new Uint8Array(0);
125
+ // Convert base58 string to number
126
+ let num = BigInt(0);
127
+ for (let i = 0; i < encoded.length; i++) {
128
+ const idx = ALPHABET.indexOf(encoded[i]);
129
+ if (idx === -1)
130
+ throw new Error(`Invalid base58 character: ${encoded[i]}`);
131
+ num = num * BigInt(58) + BigInt(idx);
132
+ }
133
+ // Convert number to bytes (big-endian, fixed width)
134
+ const result = new Uint8Array(targetLen);
135
+ for (let i = targetLen - 1; i >= 0; i--) {
136
+ result[i] = Number(num & BigInt(0xff));
137
+ num = num >> BigInt(8);
138
+ }
139
+ return result;
140
+ }
141
+ /**
142
+ * Encodes a Uint8Array using Monero's base58 scheme.
143
+ */
144
+ const cnBase58Encode = (data) => {
145
+ const fullBlocks = Math.floor(data.length / FULL_BLOCK_SIZE);
146
+ const lastBlockSize = data.length % FULL_BLOCK_SIZE;
147
+ let result = '';
148
+ for (let i = 0; i < fullBlocks; i++) {
149
+ const block = data.slice(i * FULL_BLOCK_SIZE, (i + 1) * FULL_BLOCK_SIZE);
150
+ result += encodeBlock(block);
151
+ }
152
+ if (lastBlockSize > 0) {
153
+ const lastBlock = data.slice(fullBlocks * FULL_BLOCK_SIZE);
154
+ result += encodeBlock(lastBlock);
155
+ }
156
+ return result;
157
+ };
158
+ /**
159
+ * Decodes a Monero base58-encoded string to Uint8Array.
160
+ */
161
+ const cnBase58Decode = (encoded) => {
162
+ // Figure out how many full encoded blocks and the remainder
163
+ const fullBlocks = Math.floor(encoded.length / FULL_ENCODED_BLOCK_SIZE);
164
+ const lastEncodedSize = encoded.length % FULL_ENCODED_BLOCK_SIZE;
165
+ // Find the byte length of the last block
166
+ let lastBlockSize = 0;
167
+ if (lastEncodedSize > 0) {
168
+ lastBlockSize = ENCODED_BLOCK_SIZES.indexOf(lastEncodedSize);
169
+ if (lastBlockSize === -1)
170
+ throw new Error(`Invalid encoded length: ${encoded.length}`);
171
+ }
172
+ const totalBytes = fullBlocks * FULL_BLOCK_SIZE + lastBlockSize;
173
+ const result = new Uint8Array(totalBytes);
174
+ let offset = 0;
175
+ let encodedOffset = 0;
176
+ for (let i = 0; i < fullBlocks; i++) {
177
+ const block = encoded.slice(encodedOffset, encodedOffset + FULL_ENCODED_BLOCK_SIZE);
178
+ const decoded = decodeBlock(block, FULL_BLOCK_SIZE);
179
+ result.set(decoded, offset);
180
+ offset += FULL_BLOCK_SIZE;
181
+ encodedOffset += FULL_ENCODED_BLOCK_SIZE;
182
+ }
183
+ if (lastEncodedSize > 0) {
184
+ const lastEncoded = encoded.slice(encodedOffset);
185
+ const decoded = decodeBlock(lastEncoded, lastBlockSize);
186
+ result.set(decoded, offset);
187
+ }
188
+ return result;
189
+ };
190
+
191
+ /** Network address prefixes (byte value) */
192
+ const NETWORK_PREFIXES = {
193
+ 0: 0x12, // mainnet (18)
194
+ 1: 0x35, // testnet (53)
195
+ 2: 0x18, // stagenet (24)
196
+ };
197
+ /**
198
+ * Encodes a standard Monero address from public spend/view keys.
199
+ * Format: [1-byte prefix][32-byte pubSpend][32-byte pubView][4-byte checksum]
200
+ * Result is 69 bytes → 95 base58 characters.
201
+ */
202
+ const encodeAddress = (publicSpendKey, publicViewKey, networkType) => {
203
+ const prefix = NETWORK_PREFIXES[networkType];
204
+ if (prefix === undefined)
205
+ throw new Error(`Unknown network type: ${networkType}`);
206
+ // Build the 65-byte payload: prefix + pubSpend + pubView
207
+ const payload = new Uint8Array(1 + 32 + 32);
208
+ payload[0] = prefix;
209
+ payload.set(publicSpendKey, 1);
210
+ payload.set(publicViewKey, 33);
211
+ // Checksum: first 4 bytes of keccak256(payload)
212
+ const checksum = sha3.keccak_256(payload).slice(0, 4);
213
+ // Full data: payload + checksum = 69 bytes
214
+ const full = new Uint8Array(69);
215
+ full.set(payload, 0);
216
+ full.set(checksum, 65);
217
+ return cnBase58Encode(full);
218
+ };
219
+ /**
220
+ * Decodes a standard Monero address into its public spend/view keys.
221
+ * Validates the checksum.
222
+ */
223
+ const decodeAddress = (address) => {
224
+ const data = cnBase58Decode(address);
225
+ if (data.length !== 69)
226
+ throw new Error(`Invalid address length: ${data.length}`);
227
+ const payload = data.slice(0, 65);
228
+ const checksum = data.slice(65, 69);
229
+ // Verify checksum
230
+ const expected = sha3.keccak_256(payload).slice(0, 4);
231
+ for (let i = 0; i < 4; i++) {
232
+ if (checksum[i] !== expected[i])
233
+ throw new Error('Invalid address checksum');
234
+ }
235
+ const prefix = data[0];
236
+ let networkType;
237
+ if (prefix === 0x12)
238
+ networkType = 0; // mainnet
239
+ else if (prefix === 0x35)
240
+ networkType = 1; // testnet
241
+ else if (prefix === 0x18)
242
+ networkType = 2; // stagenet
243
+ else
244
+ throw new Error(`Unknown address prefix: ${prefix}`);
245
+ return {
246
+ publicSpendKey: data.slice(1, 33),
247
+ publicViewKey: data.slice(33, 65),
248
+ networkType,
249
+ };
250
+ };
251
+
252
+ /**
253
+ * Monero network type values matching monero-ts MoneroNetworkType enum
254
+ */
255
+ const MoneroNetworkType = {
256
+ MAINNET: 0,
257
+ STAGENET: 2,
258
+ };
259
+ /**
260
+ * Maps XChainJS Network to Monero network type
261
+ */
262
+ const getMoneroNetworkType = (network) => {
263
+ const networkMap = {
264
+ [xchainClient.Network.Mainnet]: MoneroNetworkType.MAINNET,
265
+ [xchainClient.Network.Stagenet]: MoneroNetworkType.STAGENET,
266
+ [xchainClient.Network.Testnet]: MoneroNetworkType.STAGENET, // Monero testnet is deprecated; stagenet is used for testing
267
+ };
268
+ return networkMap[network];
269
+ };
270
+ /**
271
+ * Validates a Monero address format.
272
+ * Standard addresses are 95 chars, integrated addresses are 106 chars.
273
+ * Uses base58 character set and prefix validation.
274
+ */
275
+ const validateMoneroAddress = (address) => {
276
+ const base58Chars = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
277
+ if (!base58Chars.test(address))
278
+ return false;
279
+ // Standard address (95 chars): primary starts with 4/5/9, subaddress starts with 8/7/B
280
+ if (address.length === 95)
281
+ return /^[45789B]/.test(address);
282
+ // Integrated address (106 chars): starts with 4/5/A
283
+ if (address.length === 106)
284
+ return /^[45A]/.test(address);
285
+ return false;
286
+ };
287
+ /**
288
+ * Ed25519 group order (l) for sc_reduce32
289
+ */
290
+ const ED25519_ORDER = BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989');
291
+ /**
292
+ * Reduces a 32-byte value modulo the ed25519 group order.
293
+ * Required to produce valid Monero private keys from arbitrary 32-byte seeds.
294
+ */
295
+ const scReduce32 = (bytes) => {
296
+ if (bytes.length !== 32) {
297
+ throw new Error(`scReduce32 expects 32 bytes, got ${bytes.length}`);
298
+ }
299
+ // Interpret as little-endian integer
300
+ let value = BigInt(0);
301
+ for (let i = 31; i >= 0; i--) {
302
+ value = (value << BigInt(8)) | BigInt(bytes[i]);
303
+ }
304
+ // Reduce mod l
305
+ value = value % ED25519_ORDER;
306
+ // Convert back to little-endian bytes
307
+ const result = new Uint8Array(32);
308
+ let tmp = value;
309
+ for (let i = 0; i < 32; i++) {
310
+ result[i] = Number(tmp & BigInt(0xff));
311
+ tmp >>= BigInt(8);
312
+ }
313
+ return result;
314
+ };
315
+ /**
316
+ * Converts a Uint8Array to hex string
317
+ */
318
+ const bytesToHex = (bytes) => {
319
+ return Array.from(bytes)
320
+ .map((b) => b.toString(16).padStart(2, '0'))
321
+ .join('');
322
+ };
323
+ /**
324
+ * Converts a hex string to Uint8Array
325
+ */
326
+ const hexToBytes = (hex) => {
327
+ const bytes = new Uint8Array(hex.length / 2);
328
+ for (let i = 0; i < hex.length; i += 2) {
329
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
330
+ }
331
+ return bytes;
332
+ };
333
+ function bytesToBigIntLE(bytes) {
334
+ let value = BigInt(0);
335
+ for (let i = bytes.length - 1; i >= 0; i--) {
336
+ value = (value << BigInt(8)) | BigInt(bytes[i]);
337
+ }
338
+ return value;
339
+ }
340
+ function bigIntToBytes32LE(value) {
341
+ const result = new Uint8Array(32);
342
+ let tmp = value;
343
+ for (let i = 0; i < 32; i++) {
344
+ result[i] = Number(tmp & BigInt(0xff));
345
+ tmp >>= BigInt(8);
346
+ }
347
+ return result;
348
+ }
349
+ /**
350
+ * Scalar multiplication: (a * b) mod l
351
+ */
352
+ function scMul(a, b) {
353
+ const aVal = bytesToBigIntLE(a);
354
+ const bVal = bytesToBigIntLE(b);
355
+ return bigIntToBytes32LE((aVal * bVal) % ED25519_ORDER);
356
+ }
357
+ /**
358
+ * Scalar multiply-add: (a * b + c) mod l
359
+ */
360
+ function scMulAdd(a, b, c) {
361
+ const aVal = bytesToBigIntLE(a);
362
+ const bVal = bytesToBigIntLE(b);
363
+ const cVal = bytesToBigIntLE(c);
364
+ return bigIntToBytes32LE((aVal * bVal + cVal) % ED25519_ORDER);
365
+ }
366
+ /**
367
+ * Scalar multiply-subtract: (c - a * b) mod l
368
+ */
369
+ function scMulSub(a, b, c) {
370
+ const aVal = bytesToBigIntLE(a);
371
+ const bVal = bytesToBigIntLE(b);
372
+ const cVal = bytesToBigIntLE(c);
373
+ let result = (cVal - aVal * bVal) % ED25519_ORDER;
374
+ if (result < BigInt(0))
375
+ result += ED25519_ORDER;
376
+ return bigIntToBytes32LE(result);
377
+ }
378
+ /**
379
+ * Hash to scalar: keccak256(data) reduced mod l
380
+ */
381
+ function hashToScalar$1(data) {
382
+ return scReduce32(sha3.keccak_256(data));
383
+ }
384
+ /**
385
+ * Concatenate multiple Uint8Arrays
386
+ */
387
+ function concatBytes(...arrays) {
388
+ const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
389
+ const result = new Uint8Array(totalLen);
390
+ let offset = 0;
391
+ for (const arr of arrays) {
392
+ result.set(arr, offset);
393
+ offset += arr.length;
394
+ }
395
+ return result;
396
+ }
397
+
398
+ /**
399
+ * Derives an ed25519 public key from a raw private key scalar.
400
+ * Unlike standard EdDSA (which applies SHA-512 + clamping), Monero uses the raw scalar directly.
401
+ */
402
+ const secretKeyToPublicKey = (privateKey) => {
403
+ const scalar = utils.bytesToNumberLE(privateKey);
404
+ return ed25519.ed25519.ExtendedPoint.BASE.multiply(scalar).toRawBytes();
405
+ };
406
+ /**
407
+ * Derives the private view key from a private spend key.
408
+ * view_key = sc_reduce32(keccak256(spend_key))
409
+ */
410
+ const derivePrivateViewKey = (privateSpendKey) => {
411
+ const hash = sha3.keccak_256(privateSpendKey);
412
+ return scReduce32(hash);
413
+ };
414
+ /**
415
+ * Derives all four Monero keys from a private spend key.
416
+ */
417
+ const deriveKeyPairs = (privateSpendKey) => {
418
+ const publicSpendKey = secretKeyToPublicKey(privateSpendKey);
419
+ const privateViewKey = derivePrivateViewKey(privateSpendKey);
420
+ const publicViewKey = secretKeyToPublicKey(privateViewKey);
421
+ return {
422
+ privateSpendKey,
423
+ publicSpendKey,
424
+ privateViewKey,
425
+ publicViewKey,
426
+ };
427
+ };
428
+
429
+ /**
430
+ * Monero daemon HTTP JSON-RPC client.
431
+ * Provides raw RPC calls without depending on monero-ts.
432
+ */
433
+ function jsonRpc(url, method, params) {
434
+ return __awaiter(this, void 0, void 0, function* () {
435
+ const response = yield fetch(`${url}/json_rpc`, {
436
+ method: 'POST',
437
+ headers: { 'Content-Type': 'application/json' },
438
+ body: JSON.stringify({
439
+ jsonrpc: '2.0',
440
+ id: '0',
441
+ method,
442
+ params: params !== null && params !== void 0 ? params : {},
443
+ }),
444
+ });
445
+ if (!response.ok) {
446
+ throw new Error(`Daemon RPC error: ${response.status} ${response.statusText}`);
447
+ }
448
+ const data = (yield response.json());
449
+ if (data.error) {
450
+ throw new Error(`Daemon RPC error: ${data.error.message} (code ${data.error.code})`);
451
+ }
452
+ return data.result;
453
+ });
454
+ }
455
+ function httpPost(url, path, body) {
456
+ return __awaiter(this, void 0, void 0, function* () {
457
+ const response = yield fetch(`${url}${path}`, {
458
+ method: 'POST',
459
+ headers: { 'Content-Type': 'application/json' },
460
+ body: JSON.stringify(body),
461
+ });
462
+ if (!response.ok) {
463
+ throw new Error(`Daemon HTTP error: ${response.status} ${response.statusText}`);
464
+ }
465
+ return (yield response.json());
466
+ });
467
+ }
468
+ /**
469
+ * Get fee estimate from daemon.
470
+ * Returns fee per byte in piconero.
471
+ */
472
+ const getFeeEstimate = (url) => __awaiter(void 0, void 0, void 0, function* () {
473
+ const result = yield jsonRpc(url, 'get_fee_estimate');
474
+ return result.fee;
475
+ });
476
+ /**
477
+ * Get transaction details by hash(es).
478
+ */
479
+ const getTransactions = (url, txHashes) => __awaiter(void 0, void 0, void 0, function* () {
480
+ var _a;
481
+ const result = yield httpPost(url, '/get_transactions', {
482
+ txs_hashes: txHashes,
483
+ decode_as_json: false,
484
+ });
485
+ return (_a = result.txs) !== null && _a !== void 0 ? _a : [];
486
+ });
487
+ const getOuts = (url, outputs) => __awaiter(void 0, void 0, void 0, function* () {
488
+ const result = yield httpPost(url, '/get_outs', {
489
+ outputs,
490
+ get_txid: true,
491
+ });
492
+ if (result.status !== 'OK')
493
+ throw new Error(`get_outs failed: ${result.status}`);
494
+ return result.outs;
495
+ });
496
+ /**
497
+ * Get output distribution for decoy selection.
498
+ */
499
+ const getOutputDistribution = (url_1, ...args_1) => __awaiter(void 0, [url_1, ...args_1], void 0, function* (url, fromHeight = 0, toHeight = 0) {
500
+ const result = yield jsonRpc(url, 'get_output_distribution', {
501
+ amounts: [0],
502
+ cumulative: true,
503
+ from_height: fromHeight,
504
+ to_height: toHeight,
505
+ });
506
+ if (!result.distributions || result.distributions.length === 0) {
507
+ throw new Error('No output distribution data returned from daemon');
508
+ }
509
+ const dist = result.distributions[0].distribution;
510
+ return { distribution: dist.data, startHeight: dist.start_height, base: dist.base };
511
+ });
512
+ /**
513
+ * Get current blockchain height.
514
+ */
515
+ const getHeight = (url) => __awaiter(void 0, void 0, void 0, function* () {
516
+ const result = yield jsonRpc(url, 'get_block_count');
517
+ return result.count;
518
+ });
519
+ const getTransactionsDecoded = (url, txHashes) => __awaiter(void 0, void 0, void 0, function* () {
520
+ var _a;
521
+ if (txHashes.length === 0)
522
+ return [];
523
+ const result = yield httpPost(url, '/get_transactions', {
524
+ txs_hashes: txHashes,
525
+ decode_as_json: true,
526
+ });
527
+ return (_a = result.txs) !== null && _a !== void 0 ? _a : [];
528
+ });
529
+ const getBlockHeadersRange = (url, startHeight, endHeight) => __awaiter(void 0, void 0, void 0, function* () {
530
+ var _a;
531
+ const result = yield jsonRpc(url, 'get_block_headers_range', {
532
+ start_height: startHeight,
533
+ end_height: endHeight,
534
+ });
535
+ return (_a = result.headers) !== null && _a !== void 0 ? _a : [];
536
+ });
537
+ /**
538
+ * Get block tx hashes for a specific height.
539
+ * Returns just the tx_hashes array (excludes miner tx).
540
+ */
541
+ const getBlockTxHashes = (url, height) => __awaiter(void 0, void 0, void 0, function* () {
542
+ var _a;
543
+ const result = yield jsonRpc(url, 'get_block', { height });
544
+ return { txHashes: (_a = result.tx_hashes) !== null && _a !== void 0 ? _a : [], timestamp: result.block_header.timestamp };
545
+ });
546
+ /**
547
+ * Broadcast a raw transaction hex to the network.
548
+ */
549
+ const sendRawTransaction = (url, txHex) => __awaiter(void 0, void 0, void 0, function* () {
550
+ var _a;
551
+ const result = yield httpPost(url, '/send_raw_transaction', {
552
+ tx_as_hex: txHex,
553
+ do_not_relay: false,
554
+ });
555
+ if (result.status !== 'OK') {
556
+ throw new Error(`Broadcast failed: ${(_a = result.reason) !== null && _a !== void 0 ? _a : result.status}`);
557
+ }
558
+ return result;
559
+ });
560
+
561
+ /**
562
+ * MyMonero-compatible Light Wallet Server (LWS) REST API client.
563
+ * LWS accepts private view keys and returns pre-decoded balances/transactions,
564
+ * avoiding the need for client-side blockchain scanning.
565
+ */
566
+ function lwsPost(url, path, body) {
567
+ return __awaiter(this, void 0, void 0, function* () {
568
+ const response = yield fetch(`${url}${path}`, {
569
+ method: 'POST',
570
+ headers: { 'Content-Type': 'application/json' },
571
+ body: JSON.stringify(body),
572
+ });
573
+ if (!response.ok) {
574
+ throw new Error(`LWS error: ${response.status} ${response.statusText}`);
575
+ }
576
+ return (yield response.json());
577
+ });
578
+ }
579
+ /**
580
+ * Register wallet address with LWS for scanning.
581
+ * Must be called before other endpoints will return data.
582
+ */
583
+ const login = (url, address, viewKeyHex) => __awaiter(void 0, void 0, void 0, function* () {
584
+ return lwsPost(url, '/login', {
585
+ address,
586
+ view_key: viewKeyHex,
587
+ create_account: true,
588
+ generated_locally: false,
589
+ });
590
+ });
591
+ /**
592
+ * Get balance summary for an address.
593
+ * Returns total received/sent in piconero strings.
594
+ */
595
+ const getAddressInfo = (url, address, viewKeyHex) => __awaiter(void 0, void 0, void 0, function* () {
596
+ return lwsPost(url, '/get_address_info', {
597
+ address,
598
+ view_key: viewKeyHex,
599
+ });
600
+ });
601
+ /**
602
+ * Get transaction history for an address.
603
+ * Returns transactions with per-tx received/sent amounts.
604
+ */
605
+ const getAddressTxs = (url, address, viewKeyHex) => __awaiter(void 0, void 0, void 0, function* () {
606
+ return lwsPost(url, '/get_address_txs', {
607
+ address,
608
+ view_key: viewKeyHex,
609
+ });
610
+ });
611
+ /**
612
+ * Get unspent outputs for transaction building.
613
+ */
614
+ const getUnspentOuts = (url_1, address_1, viewKeyHex_1, ...args_1) => __awaiter(void 0, [url_1, address_1, viewKeyHex_1, ...args_1], void 0, function* (url, address, viewKeyHex, amount = '0') {
615
+ return lwsPost(url, '/get_unspent_outs', {
616
+ address,
617
+ view_key: viewKeyHex,
618
+ amount,
619
+ mixin: 15,
620
+ use_dust: false,
621
+ dust_threshold: '2000000000',
622
+ });
623
+ });
624
+
625
+ /**
626
+ * Monero's hash_to_ec / ge_fromfe_frombytes_vartime implementation.
627
+ * Ported from CoinSpace/monerolib (pure JS Monero library).
628
+ *
629
+ * Maps arbitrary data to an ed25519 point. This is NOT standard hash-to-curve
630
+ * (RFC 9380) — it matches Monero's specific algorithm from crypto-ops.c.
631
+ */
632
+ const ExtPoint$6 = ed25519.ed25519.ExtendedPoint;
633
+ // ed25519 field prime: 2^255 - 19
634
+ const P = BigInt('57896044618658097711785492504343953926634992332820282019728792003956564819949');
635
+ function mod(a) {
636
+ const r = a % P;
637
+ return r >= BigInt(0) ? r : r + P;
638
+ }
639
+ const A = BigInt(486662);
640
+ const A_SQUARED = mod(A * A);
641
+ // Precomputed constants (big-endian hex from monerolib/Monero C code)
642
+ const SQRTM1 = BigInt('0x547cdb7fb03e20f4d4b2ff66c2042858d0bce7f952d01b873b11e4d8b5f15f3d');
643
+ const FFFB1 = BigInt('0x7e71fbefdad61b1720a9c53741fb19e3d19404a8b92a738d22a76975321c41ee');
644
+ const FFFB2 = BigInt('0x32f9e1f5fba5d3096e2bae483fe9a041ae21fcb9fba908202d219b7c9f83650d');
645
+ const FFFB3 = BigInt('0x1a43f3031067dbf926c0f4887ef7432eee46fc08a13f4a49853d1903b6b39186');
646
+ const FFFB4 = BigInt('0x674a110d14c208efb89546403f0da2ed4024ff4ea5964229581b7d8717302c66');
647
+ function modPow(base, exp) {
648
+ let result = BigInt(1);
649
+ base = mod(base);
650
+ while (exp > BigInt(0)) {
651
+ if (exp & BigInt(1)) {
652
+ result = mod(result * base);
653
+ }
654
+ exp >>= BigInt(1);
655
+ base = mod(base * base);
656
+ }
657
+ return result;
658
+ }
659
+ /**
660
+ * fe_divpowm1: computes (u/v)^((p+3)/8)
661
+ * Formula: u * v^3 * (u * v^7)^((p-5)/8)
662
+ */
663
+ function divPowM1(u, v) {
664
+ const v3 = mod(v * mod(v * v));
665
+ const v7 = mod(v3 * mod(v3 * v));
666
+ const uv7 = mod(u * v7);
667
+ const exp = (P - BigInt(5)) >> BigInt(3); // (p-5)/8
668
+ const t = modPow(uv7, exp);
669
+ return mod(u * mod(v3 * t));
670
+ }
671
+ /**
672
+ * Hash arbitrary data to an ed25519 point (Monero's hash_to_ec).
673
+ * Algorithm: ge_fromfe_frombytes_vartime
674
+ */
675
+ function hashToPoint(data) {
676
+ const hash = sha3.keccak_256(data);
677
+ const u = mod(utils.bytesToNumberLE(hash));
678
+ // v = 2*u^2
679
+ const v = mod(BigInt(2) * mod(u * u));
680
+ // w = 1 + 2*u^2
681
+ const w = mod(v + BigInt(1));
682
+ // t = w^2 - A^2*v
683
+ const t = mod(mod(w * w) - mod(A_SQUARED * v));
684
+ // x = (w/t)^((p+3)/8) via divPowM1
685
+ let x = divPowM1(w, t);
686
+ let negative = false;
687
+ // Check: w - x^2*t == 0?
688
+ let check = mod(w - mod(mod(x * x) * t));
689
+ if (check !== BigInt(0)) {
690
+ // Check: w + x^2*t == 0?
691
+ check = mod(w + mod(mod(x * x) * t));
692
+ if (check !== BigInt(0)) {
693
+ negative = true;
694
+ }
695
+ else {
696
+ x = mod(x * FFFB1);
697
+ }
698
+ }
699
+ else {
700
+ x = mod(x * FFFB2);
701
+ }
702
+ let odd;
703
+ let r;
704
+ if (!negative) {
705
+ odd = false;
706
+ r = mod(P - mod(A * v)); // r = -A*v
707
+ x = mod(x * u); // x *= u
708
+ }
709
+ else {
710
+ odd = true;
711
+ r = mod(P - A); // r = -A
712
+ // Check with sqrtm1
713
+ check = mod(w - mod(mod(mod(x * x) * t) * SQRTM1));
714
+ if (check !== BigInt(0)) {
715
+ check = mod(w + mod(mod(mod(x * x) * t) * SQRTM1));
716
+ if (check !== BigInt(0)) {
717
+ throw new Error('hashToPoint: invalid point (should never happen)');
718
+ }
719
+ else {
720
+ x = mod(x * FFFB3);
721
+ }
722
+ }
723
+ else {
724
+ x = mod(x * FFFB4);
725
+ }
726
+ }
727
+ // Adjust sign
728
+ const xIsOdd = (x & BigInt(1)) === BigInt(1);
729
+ if (xIsOdd !== odd) {
730
+ x = mod(P - x);
731
+ }
732
+ // Projective coordinates: (X, Y, Z) where affine = (X/Z, Y/Z)
733
+ const z = mod(r + w);
734
+ const y = mod(r - w);
735
+ const X = mod(x * z);
736
+ // Convert projective (X, Y, Z) to affine and encode
737
+ const zInv = modPow(z, P - BigInt(2));
738
+ const xAff = mod(X * zInv);
739
+ const yAff = mod(y * zInv);
740
+ // Encode as ed25519 point bytes
741
+ const yBytes = utils.numberToBytesLE(yAff, 32);
742
+ if (xAff & BigInt(1)) {
743
+ yBytes[31] |= 0x80;
744
+ }
745
+ const point = ExtPoint$6.fromHex(yBytes);
746
+ // Cofactor clearing: multiply by 8 (Monero's hash_to_ec = ge_fromfe + mul8)
747
+ return point.multiply(BigInt(8));
748
+ }
749
+
750
+ /**
751
+ * CLSAG (Concise Linkable Spontaneous Anonymous Group) signature.
752
+ * Used in Monero RingCT for input authorization + commitment balance proof.
753
+ *
754
+ * Reference: monero/src/ringct/rctSigs.cpp (CLSAG_Gen, verRctCLSAGSimple)
755
+ */
756
+ const ExtPoint$5 = ed25519.ed25519.ExtendedPoint;
757
+ // Domain separators (ASCII zero-padded to 32 bytes)
758
+ function domainTag(s) {
759
+ const tag = new Uint8Array(32);
760
+ const encoded = new TextEncoder().encode(s);
761
+ tag.set(encoded);
762
+ return tag;
763
+ }
764
+ const CLSAG_AGG_0 = domainTag('CLSAG_agg_0');
765
+ const CLSAG_AGG_1 = domainTag('CLSAG_agg_1');
766
+ const CLSAG_ROUND = domainTag('CLSAG_round');
767
+ // INV_EIGHT: modular inverse of 8 mod l (ed25519 group order)
768
+ const INV_EIGHT_HEX = '792fdce229e50661d0da1c7db39dd30700000000000000000000000000000006';
769
+ const INV_EIGHT$1 = hexToBytes(INV_EIGHT_HEX);
770
+ /**
771
+ * Generate a random 32-byte scalar reduced mod l.
772
+ */
773
+ function randomScalar$2() {
774
+ const bytes = new Uint8Array(32);
775
+ crypto.getRandomValues(bytes);
776
+ return scReduce32(bytes);
777
+ }
778
+ /**
779
+ * CLSAG signing.
780
+ *
781
+ * @param message - 32-byte transaction prefix hash
782
+ * @param ring - Ring members (dest + mask for each)
783
+ * @param Cout - Pseudo-output commitment (C_offset)
784
+ * @param secretKey - Signer's one-time private key (p)
785
+ * @param secretMask - Commitment mask difference: z = input_mask - pseudo_out_mask
786
+ * @param realIndex - Index of real signer in ring
787
+ * @param keyImage - Pre-computed key image (p * Hp(P[l]))
788
+ * @returns CLSAG signature
789
+ */
790
+ function clsagSign(message, ring, Cout, secretKey, secretMask, realIndex, keyImage) {
791
+ const n = ring.length;
792
+ if (n === 0)
793
+ throw new Error('CLSAG: ring must not be empty');
794
+ if (realIndex < 0 || realIndex >= n)
795
+ throw new Error(`CLSAG: realIndex ${realIndex} out of range [0, ${n})`);
796
+ const I = ExtPoint$5.fromHex(keyImage);
797
+ // Hp(P[l]) — hash of real signer's public key
798
+ const H = hashToPoint(ring[realIndex].dest);
799
+ // D_full = z * Hp(P[l])
800
+ const zScalar = utils.bytesToNumberLE(secretMask);
801
+ const D_full = H.multiply(zScalar);
802
+ // sig.D = (1/8) * D_full
803
+ const invEightScalar = utils.bytesToNumberLE(INV_EIGHT$1);
804
+ const D = D_full.multiply(invEightScalar);
805
+ const DBytes = D.toRawBytes();
806
+ // Extract ring keys as byte arrays
807
+ const P = ring.map((m) => m.dest);
808
+ const C_nonzero = ring.map((m) => m.mask);
809
+ // Compute aggregation coefficients mu_P, mu_C
810
+ const aggParts = [...P.map((p) => p), ...C_nonzero.map((c) => c), keyImage, DBytes, Cout];
811
+ const muP = hashToScalar$1(concatBytes(CLSAG_AGG_0, ...aggParts));
812
+ const muC = hashToScalar$1(concatBytes(CLSAG_AGG_1, ...aggParts));
813
+ // Round hash prefix (constant across all rounds)
814
+ const roundPrefix = concatBytes(CLSAG_ROUND, ...P, ...C_nonzero, Cout, message);
815
+ // Step 3: Generate nonce
816
+ const alpha = randomScalar$2();
817
+ const alphaScalar = utils.bytesToNumberLE(alpha);
818
+ const aG = ExtPoint$5.BASE.multiply(alphaScalar);
819
+ const aH = H.multiply(alphaScalar);
820
+ // Initial challenge at signer's position
821
+ let c = hashToScalar$1(concatBytes(roundPrefix, aG.toRawBytes(), aH.toRawBytes()));
822
+ // Precompute C_offset point
823
+ const CoutPoint = ExtPoint$5.fromHex(Cout);
824
+ // Ring loop
825
+ const s = new Array(n);
826
+ let c1;
827
+ let i = (realIndex + 1) % n;
828
+ if (i === 0)
829
+ c1 = c;
830
+ while (i !== realIndex) {
831
+ s[i] = randomScalar$2();
832
+ const sScalar = utils.bytesToNumberLE(s[i]);
833
+ const cP = scMul(c, muP);
834
+ const cC = scMul(c, muC);
835
+ const cPScalar = utils.bytesToNumberLE(cP);
836
+ const cCScalar = utils.bytesToNumberLE(cC);
837
+ const Pi = ExtPoint$5.fromHex(P[i]);
838
+ const Ci = ExtPoint$5.fromHex(C_nonzero[i]).subtract(CoutPoint);
839
+ // L = s[i]*G + c_p*P[i] + c_c*C[i]
840
+ const L = ExtPoint$5.BASE.multiply(sScalar).add(Pi.multiply(cPScalar)).add(Ci.multiply(cCScalar));
841
+ // R = s[i]*Hp(P[i]) + c_p*I + c_c*D_full
842
+ const HpPi = hashToPoint(P[i]);
843
+ const R = HpPi.multiply(sScalar).add(I.multiply(cPScalar)).add(D_full.multiply(cCScalar));
844
+ c = hashToScalar$1(concatBytes(roundPrefix, L.toRawBytes(), R.toRawBytes()));
845
+ i = (i + 1) % n;
846
+ if (i === 0)
847
+ c1 = c;
848
+ }
849
+ // Final response: s[l] = alpha - c * (mu_P * p + mu_C * z)
850
+ const muPp = scMul(muP, secretKey);
851
+ const combined = scMulAdd(muC, secretMask, muPp);
852
+ s[realIndex] = scMulSub(c, combined, alpha);
853
+ if (c1 === undefined)
854
+ c1 = c;
855
+ return { s, c1, D: DBytes };
856
+ }
857
+
858
+ /**
859
+ * Pedersen commitment scheme for Monero.
860
+ * C = mask*G + amount*H
861
+ */
862
+ const ExtPoint$4 = ed25519.ed25519.ExtendedPoint;
863
+ // Monero's second generator H, hardcoded from rctTypes.h.
864
+ // Historically derived via toPoint(cn_fast_hash(G)), now a consensus constant.
865
+ const H_HEX = '8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94';
866
+ let _H = null;
867
+ /**
868
+ * Get the Pedersen generator H.
869
+ */
870
+ function getH() {
871
+ if (_H === null) {
872
+ _H = ExtPoint$4.fromHex(H_HEX);
873
+ }
874
+ return _H;
875
+ }
876
+ /**
877
+ * Create a Pedersen commitment: C = mask*G + amount*H
878
+ * @param mask - Blinding factor (32-byte scalar LE)
879
+ * @param amount - Amount to commit to (bigint)
880
+ */
881
+ function commit(mask, amount) {
882
+ const maskScalar = utils.bytesToNumberLE(mask);
883
+ const H = getH();
884
+ const maskG = ExtPoint$4.BASE.multiply(maskScalar);
885
+ const amountH = H.multiply(amount);
886
+ return maskG.add(amountH);
887
+ }
888
+
889
+ /**
890
+ * Bulletproofs+ range proof implementation for Monero.
891
+ * Proves amounts are in [0, 2^64) without revealing them.
892
+ *
893
+ * Reference: monero/src/ringct/bulletproofs_plus.cc
894
+ */
895
+ const ExtPoint$3 = ed25519.ed25519.ExtendedPoint;
896
+ // Ed25519 group order
897
+ const L$1 = BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989');
898
+ const ZERO = BigInt(0);
899
+ const ONE = BigInt(1);
900
+ const TWO = BigInt(2);
901
+ BigInt(8);
902
+ const maxN = 64; // bits per value
903
+ const maxM = 16; // max aggregated outputs
904
+ function modL(x) {
905
+ const r = x % L$1;
906
+ return r < ZERO ? r + L$1 : r;
907
+ }
908
+ // INV_EIGHT = modular inverse of 8 mod l
909
+ const INV_EIGHT = utils.bytesToNumberLE(hexToBytes('792fdce229e50661d0da1c7db39dd30700000000000000000000000000000006'));
910
+ const MINUS_INV_EIGHT = modL(L$1 - INV_EIGHT);
911
+ // Domain separator for generator derivation
912
+ const EXPONENT_TAG = new TextEncoder().encode('bulletproof_plus');
913
+ // Domain separator for transcript
914
+ const TRANSCRIPT_TAG = new TextEncoder().encode('bulletproof_plus_transcript');
915
+ // Varint encoding (same as tx/serialize)
916
+ function encodeVarint$2(n) {
917
+ const bytes = [];
918
+ let val = n;
919
+ while (val >= 0x80) {
920
+ bytes.push((val & 0x7f) | 0x80);
921
+ val >>>= 7;
922
+ }
923
+ bytes.push(val & 0x7f);
924
+ return new Uint8Array(bytes);
925
+ }
926
+ // --- Generator cache ---
927
+ let _generators = null;
928
+ function getExponent(base, idx) {
929
+ const data = concatBytes(base, EXPONENT_TAG, encodeVarint$2(idx));
930
+ const hash = sha3.keccak_256(data);
931
+ return hashToPoint(hash);
932
+ }
933
+ function getGenerators(count) {
934
+ if (_generators !== null && _generators.Gi.length >= count)
935
+ return _generators;
936
+ const H = getH();
937
+ const hBytes = H.toRawBytes();
938
+ const Gi = new Array(count);
939
+ const Hi = new Array(count);
940
+ for (let i = 0; i < count; i++) {
941
+ Hi[i] = getExponent(hBytes, i * 2);
942
+ Gi[i] = getExponent(hBytes, i * 2 + 1);
943
+ }
944
+ _generators = { Gi, Hi };
945
+ return _generators;
946
+ }
947
+ // --- Initial transcript (cached) ---
948
+ let _initTranscript = null;
949
+ function getInitialTranscript() {
950
+ if (_initTranscript !== null)
951
+ return _initTranscript;
952
+ const hash = sha3.keccak_256(TRANSCRIPT_TAG);
953
+ const point = hashToPoint(hash);
954
+ _initTranscript = point.toRawBytes();
955
+ return _initTranscript;
956
+ }
957
+ // --- Transcript updates ---
958
+ function transcriptUpdate1(transcript, update0) {
959
+ return hashToScalar$1(concatBytes(transcript, update0));
960
+ }
961
+ function transcriptUpdate2(transcript, update0, update1) {
962
+ return hashToScalar$1(concatBytes(transcript, update0, update1));
963
+ }
964
+ // --- Scalar helpers ---
965
+ function scalarToBytes(s) {
966
+ return new Uint8Array(utils.numberToBytesLE(s, 32));
967
+ }
968
+ function randomScalar$1() {
969
+ const bytes = new Uint8Array(32);
970
+ crypto.getRandomValues(bytes);
971
+ return utils.bytesToNumberLE(scReduce32(bytes));
972
+ }
973
+ function scalarInvert(x) {
974
+ // Fermat's little theorem: x^(l-2) mod l
975
+ let result = ONE;
976
+ let base = modL(x);
977
+ let exp = L$1 - TWO;
978
+ while (exp > ZERO) {
979
+ if (exp & ONE)
980
+ result = modL(result * base);
981
+ exp >>= ONE;
982
+ base = modL(base * base);
983
+ }
984
+ return result;
985
+ }
986
+ function nextPow2(n) {
987
+ let p = 1;
988
+ while (p < n)
989
+ p <<= 1;
990
+ return p;
991
+ }
992
+ function log2(n) {
993
+ let r = 0;
994
+ let v = n;
995
+ while (v > 1) {
996
+ v >>= 1;
997
+ r++;
998
+ }
999
+ return r;
1000
+ }
1001
+ // Weighted inner product: WIP(a, b, y) = sum(a[i] * b[i] * y^(i+1))
1002
+ function weightedInnerProduct(a, b, y) {
1003
+ let result = ZERO;
1004
+ let yPow = ONE;
1005
+ for (let i = 0; i < a.length; i++) {
1006
+ yPow = modL(yPow * y);
1007
+ result = modL(result + modL(modL(a[i] * b[i]) * yPow));
1008
+ }
1009
+ return result;
1010
+ }
1011
+ // Compute L or R in the inner product argument
1012
+ function computeLR(size, yFactor, G, gOff, H, hOff, a, aOff, b, bOff, c, d) {
1013
+ // Multi-scalar multiplication
1014
+ let result = ExtPoint$3.BASE.multiply(modL(d * INV_EIGHT)).add(getH().multiply(modL(c * INV_EIGHT)));
1015
+ for (let i = 0; i < size; i++) {
1016
+ const aScaled = modL(a[aOff + i] * modL(yFactor * INV_EIGHT));
1017
+ const bScaled = modL(b[bOff + i] * INV_EIGHT);
1018
+ result = result.add(G[gOff + i].multiply(aScaled));
1019
+ result = result.add(H[hOff + i].multiply(bScaled));
1020
+ }
1021
+ return result;
1022
+ }
1023
+ /**
1024
+ * Generate a Bulletproofs+ range proof.
1025
+ *
1026
+ * @param amounts - Array of amounts (bigint, each < 2^64)
1027
+ * @param masks - Array of blinding factors (32-byte scalars)
1028
+ * @returns BP+ proof
1029
+ */
1030
+ function bulletproofPlusProve(amounts, masks) {
1031
+ const m = amounts.length;
1032
+ if (m === 0 || m > maxM)
1033
+ throw new Error(`BP+: invalid output count ${m}`);
1034
+ const M = nextPow2(m);
1035
+ const logM = log2(M);
1036
+ const MN = M * maxN;
1037
+ const logMN = logM + log2(maxN);
1038
+ // Get generators
1039
+ const gens = getGenerators(MN);
1040
+ const { Gi, Hi } = gens;
1041
+ // Compute V[i] = (gamma*INV_EIGHT)*G + (sv*INV_EIGHT)*H
1042
+ const V = [];
1043
+ const sv = [];
1044
+ const gamma = [];
1045
+ for (let i = 0; i < m; i++) {
1046
+ sv.push(amounts[i]);
1047
+ gamma.push(utils.bytesToNumberLE(masks[i]));
1048
+ const gScalar = modL(gamma[i] * INV_EIGHT);
1049
+ const hScalar = modL(sv[i] * INV_EIGHT);
1050
+ const point = ExtPoint$3.BASE.multiply(gScalar).add(getH().multiply(hScalar));
1051
+ V.push(point.toRawBytes());
1052
+ }
1053
+ // Bit decomposition
1054
+ const aL = new Array(MN);
1055
+ const aR = new Array(MN);
1056
+ const aL8 = new Array(MN);
1057
+ const aR8 = new Array(MN);
1058
+ for (let j = 0; j < M; j++) {
1059
+ for (let i = 0; i < maxN; i++) {
1060
+ const idx = j * maxN + i;
1061
+ if (j < m && ((sv[j] >> BigInt(i)) & ONE) === ONE) {
1062
+ aL[idx] = ONE;
1063
+ aR[idx] = ZERO;
1064
+ aL8[idx] = INV_EIGHT;
1065
+ aR8[idx] = ZERO;
1066
+ }
1067
+ else {
1068
+ aL[idx] = ZERO;
1069
+ aR[idx] = modL(L$1 - ONE); // -1 mod l
1070
+ aL8[idx] = ZERO;
1071
+ aR8[idx] = MINUS_INV_EIGHT;
1072
+ }
1073
+ }
1074
+ }
1075
+ // Transcript initialization
1076
+ let transcript = getInitialTranscript();
1077
+ const vConcat = concatBytes(...V);
1078
+ transcript = scalarToBytes(utils.bytesToNumberLE(transcriptUpdate1(transcript, hashToScalar$1(vConcat))));
1079
+ // Commitment A
1080
+ const alpha = randomScalar$1();
1081
+ // pre_A = sum(aL8[i]*Gi[i] + aR8[i]*Hi[i])
1082
+ let preA = ExtPoint$3.ZERO;
1083
+ for (let i = 0; i < MN; i++) {
1084
+ if (aL8[i] !== ZERO)
1085
+ preA = preA.add(Gi[i].multiply(aL8[i]));
1086
+ if (aR8[i] !== ZERO)
1087
+ preA = preA.add(Hi[i].multiply(aR8[i]));
1088
+ }
1089
+ const A = preA.add(ExtPoint$3.BASE.multiply(modL(alpha * INV_EIGHT)));
1090
+ const ABytes = A.toRawBytes();
1091
+ // Challenge y
1092
+ const yBytes = transcriptUpdate1(transcript, ABytes);
1093
+ const y = utils.bytesToNumberLE(yBytes);
1094
+ if (y === ZERO)
1095
+ throw new Error('BP+: y is zero');
1096
+ // Challenge z
1097
+ const zBytes = hashToScalar$1(yBytes);
1098
+ const z = utils.bytesToNumberLE(zBytes);
1099
+ transcript = zBytes;
1100
+ if (z === ZERO)
1101
+ throw new Error('BP+: z is zero');
1102
+ const zSq = modL(z * z);
1103
+ // d vector: d[j*N+i] = z^(2*(j+1)) * 2^i
1104
+ const d = new Array(MN);
1105
+ d[0] = zSq;
1106
+ for (let i = 1; i < maxN; i++) {
1107
+ d[i] = modL(d[i - 1] * TWO);
1108
+ }
1109
+ for (let j = 1; j < M; j++) {
1110
+ for (let i = 0; i < maxN; i++) {
1111
+ d[j * maxN + i] = modL(d[(j - 1) * maxN + i] * zSq);
1112
+ }
1113
+ }
1114
+ // y powers: y^0 to y^(MN+1)
1115
+ const yPowers = new Array(MN + 2);
1116
+ yPowers[0] = ONE;
1117
+ for (let i = 1; i <= MN + 1; i++) {
1118
+ yPowers[i] = modL(yPowers[i - 1] * y);
1119
+ }
1120
+ // aL1 = aL - z, aR1 = aR + z + d[i]*y^(MN-i)
1121
+ const aL1 = new Array(MN);
1122
+ const aR1 = new Array(MN);
1123
+ for (let i = 0; i < MN; i++) {
1124
+ aL1[i] = modL(aL[i] - z);
1125
+ aR1[i] = modL(modL(aR[i] + z) + modL(d[i] * yPowers[MN - i]));
1126
+ }
1127
+ // alpha1 = alpha + sum(y^(MN+1) * z^(2*(j+1)) * gamma[j])
1128
+ let alpha1 = alpha;
1129
+ let temp = ONE;
1130
+ for (let j = 0; j < m; j++) {
1131
+ temp = modL(temp * zSq);
1132
+ alpha1 = modL(alpha1 + modL(modL(yPowers[MN + 1] * temp) * gamma[j]));
1133
+ }
1134
+ // Inner-product argument
1135
+ let nprime = MN;
1136
+ const Gprime = Gi.slice(0, MN);
1137
+ const Hprime = Hi.slice(0, MN);
1138
+ let aprime = aL1.slice();
1139
+ let bprime = aR1.slice();
1140
+ const yInv = scalarInvert(y);
1141
+ const yInvPow = new Array(MN);
1142
+ yInvPow[0] = ONE;
1143
+ for (let i = 1; i < MN; i++) {
1144
+ yInvPow[i] = modL(yInvPow[i - 1] * yInv);
1145
+ }
1146
+ const Ls = [];
1147
+ const Rs = [];
1148
+ for (let round = 0; round < logMN; round++) {
1149
+ nprime = nprime >> 1;
1150
+ const cL = weightedInnerProduct(aprime.slice(0, nprime), bprime.slice(nprime, nprime * 2), y);
1151
+ // For cR: aprime[nprime..] scaled by y^nprime
1152
+ const aScaled = new Array(nprime);
1153
+ for (let i = 0; i < nprime; i++) {
1154
+ aScaled[i] = modL(aprime[nprime + i] * yPowers[nprime]);
1155
+ }
1156
+ const cR = weightedInnerProduct(aScaled, bprime.slice(0, nprime), y);
1157
+ const dL = randomScalar$1();
1158
+ const dR = randomScalar$1();
1159
+ const LPoint = computeLR(nprime, yInvPow[nprime], Gprime, nprime, Hprime, 0, aprime, 0, bprime, nprime, cL, dL);
1160
+ const RPoint = computeLR(nprime, yPowers[nprime], Gprime, 0, Hprime, nprime, aprime, nprime, bprime, 0, cR, dR);
1161
+ const LBytes = LPoint.toRawBytes();
1162
+ const RBytes = RPoint.toRawBytes();
1163
+ Ls.push(LBytes);
1164
+ Rs.push(RBytes);
1165
+ const eBytes = transcriptUpdate2(transcript, LBytes, RBytes);
1166
+ const e = utils.bytesToNumberLE(eBytes);
1167
+ if (e === ZERO)
1168
+ throw new Error('BP+: challenge is zero');
1169
+ transcript = eBytes;
1170
+ const eInv = scalarInvert(e);
1171
+ // Fold generators
1172
+ const tempG = modL(yInvPow[nprime] * e);
1173
+ const newGprime = new Array(nprime);
1174
+ const newHprime = new Array(nprime);
1175
+ for (let i = 0; i < nprime; i++) {
1176
+ newGprime[i] = Gprime[i].multiply(eInv).add(Gprime[nprime + i].multiply(tempG));
1177
+ newHprime[i] = Hprime[i].multiply(e).add(Hprime[nprime + i].multiply(eInv));
1178
+ }
1179
+ // Fold scalar vectors
1180
+ const tempA = modL(eInv * yPowers[nprime]);
1181
+ const newAprime = new Array(nprime);
1182
+ const newBprime = new Array(nprime);
1183
+ for (let i = 0; i < nprime; i++) {
1184
+ newAprime[i] = modL(modL(e * aprime[i]) + modL(tempA * aprime[nprime + i]));
1185
+ newBprime[i] = modL(modL(eInv * bprime[i]) + modL(e * bprime[nprime + i]));
1186
+ }
1187
+ // Update alpha1
1188
+ alpha1 = modL(alpha1 + modL(dL * modL(e * e)) + modL(dR * modL(eInv * eInv)));
1189
+ // Replace arrays
1190
+ Gprime.length = nprime;
1191
+ Hprime.length = nprime;
1192
+ for (let i = 0; i < nprime; i++) {
1193
+ Gprime[i] = newGprime[i];
1194
+ Hprime[i] = newHprime[i];
1195
+ }
1196
+ aprime = newAprime;
1197
+ bprime = newBprime;
1198
+ }
1199
+ // Final round
1200
+ const r = randomScalar$1();
1201
+ const s = randomScalar$1();
1202
+ const d_ = randomScalar$1();
1203
+ const eta = randomScalar$1();
1204
+ const rysb = modL(modL(r * modL(y * bprime[0])) + modL(s * modL(y * aprime[0])));
1205
+ const A1 = Gprime[0]
1206
+ .multiply(modL(r * INV_EIGHT))
1207
+ .add(Hprime[0].multiply(modL(s * INV_EIGHT)))
1208
+ .add(ExtPoint$3.BASE.multiply(modL(d_ * INV_EIGHT)))
1209
+ .add(getH().multiply(modL(rysb * INV_EIGHT)));
1210
+ const A1Bytes = A1.toRawBytes();
1211
+ const B = ExtPoint$3.BASE.multiply(modL(eta * INV_EIGHT)).add(getH().multiply(modL(modL(r * modL(y * s)) * INV_EIGHT)));
1212
+ const BBytes = B.toRawBytes();
1213
+ // Final challenge
1214
+ const eFinalBytes = transcriptUpdate2(transcript, A1Bytes, BBytes);
1215
+ const eFinal = utils.bytesToNumberLE(eFinalBytes);
1216
+ if (eFinal === ZERO)
1217
+ throw new Error('BP+: final challenge is zero');
1218
+ const eFinalSq = modL(eFinal * eFinal);
1219
+ const r1 = scalarToBytes(modL(modL(aprime[0] * eFinal) + r));
1220
+ const s1 = scalarToBytes(modL(modL(bprime[0] * eFinal) + s));
1221
+ const d1 = scalarToBytes(modL(eta + modL(d_ * eFinal) + modL(alpha1 * eFinalSq)));
1222
+ return { A: ABytes, A1: A1Bytes, B: BBytes, r1, s1, d1, L: Ls, R: Rs };
1223
+ }
1224
+
1225
+ /**
1226
+ * Key image generation for Monero ring signatures.
1227
+ * KI = x * Hp(P) where x is the private key and P is the public key.
1228
+ */
1229
+ /**
1230
+ * Generate a key image from a private/public key pair.
1231
+ * KI = x * Hp(P)
1232
+ * @param privKey - Private key (32-byte scalar LE)
1233
+ * @param pubKey - Corresponding public key (32-byte compressed point)
1234
+ * @returns Key image as 32-byte compressed point
1235
+ */
1236
+ function generateKeyImage(privKey, pubKey) {
1237
+ const hp = hashToPoint(pubKey);
1238
+ const scalar = utils.bytesToNumberLE(privKey);
1239
+ return hp.multiply(scalar).toRawBytes();
1240
+ }
1241
+
1242
+ /**
1243
+ * Monero stealth address (one-time address) derivation.
1244
+ *
1245
+ * Output key: P = Hs(r*A || i)*G + B
1246
+ * - r: tx private key
1247
+ * - A: recipient's public view key
1248
+ * - B: recipient's public spend key
1249
+ * - i: output index
1250
+ * - Hs: keccak256 -> sc_reduce32 (hash to scalar)
1251
+ *
1252
+ * Input key recovery: x = Hs(a*R || i) + b
1253
+ * - a: recipient's private view key
1254
+ * - R: tx public key (r*G)
1255
+ * - b: recipient's private spend key
1256
+ */
1257
+ const ExtPoint$2 = ed25519.ed25519.ExtendedPoint;
1258
+ /**
1259
+ * Hash to scalar: Hs(data) = sc_reduce32(keccak256(data))
1260
+ */
1261
+ function hashToScalar(data) {
1262
+ return scReduce32(sha3.keccak_256(data));
1263
+ }
1264
+ function encodeVarint$1(n) {
1265
+ const bytes = [];
1266
+ while (n >= 0x80) {
1267
+ bytes.push((n & 0x7f) | 0x80);
1268
+ n >>>= 7;
1269
+ }
1270
+ bytes.push(n & 0x7f);
1271
+ return new Uint8Array(bytes);
1272
+ }
1273
+ /**
1274
+ * Concatenate buffers with a varint output index.
1275
+ */
1276
+ function deriveKeyData(sharedSecret, outputIndex) {
1277
+ const secretBytes = sharedSecret.toRawBytes();
1278
+ // Encode output index as varint (simple case: single byte for index < 128)
1279
+ const indexBytes = encodeVarint$1(outputIndex);
1280
+ const combined = new Uint8Array(secretBytes.length + indexBytes.length);
1281
+ combined.set(secretBytes, 0);
1282
+ combined.set(indexBytes, secretBytes.length);
1283
+ return combined;
1284
+ }
1285
+ /**
1286
+ * Derive the one-time output public key for a transaction output.
1287
+ * P = Hs(r*A || i)*G + B
1288
+ *
1289
+ * @param txPrivKey - Transaction private key r (32 bytes)
1290
+ * @param recipPubViewKey - Recipient's public view key A (32 bytes)
1291
+ * @param recipPubSpendKey - Recipient's public spend key B (32 bytes)
1292
+ * @param outputIndex - Output index i
1293
+ * @returns One-time public key P (32 bytes)
1294
+ */
1295
+ function deriveOutputKey(txPrivKey, recipPubViewKey, recipPubSpendKey, outputIndex) {
1296
+ const rScalar = utils.bytesToNumberLE(txPrivKey);
1297
+ const A = ExtPoint$2.fromHex(recipPubViewKey);
1298
+ const B = ExtPoint$2.fromHex(recipPubSpendKey);
1299
+ // Monero derivation: 8*r*A (cofactor multiplication)
1300
+ const rA = A.multiply(rScalar).multiply(BigInt(8));
1301
+ // Hs(r*A || i)
1302
+ const keyData = deriveKeyData(rA, outputIndex);
1303
+ const hsBytes = hashToScalar(keyData);
1304
+ const hs = utils.bytesToNumberLE(hsBytes);
1305
+ // P = Hs(r*A || i)*G + B
1306
+ const hsG = ExtPoint$2.BASE.multiply(hs);
1307
+ return hsG.add(B).toRawBytes();
1308
+ }
1309
+ /**
1310
+ * Derive the one-time private key for spending a received output.
1311
+ * x = Hs(a*R || i) + b
1312
+ *
1313
+ * @param txPubKey - Transaction public key R = r*G (32 bytes)
1314
+ * @param privViewKey - Recipient's private view key a (32 bytes)
1315
+ * @param privSpendKey - Recipient's private spend key b (32 bytes)
1316
+ * @param outputIndex - Output index i
1317
+ * @returns One-time private key x (32 bytes)
1318
+ */
1319
+ function deriveInputKey(txPubKey, privViewKey, privSpendKey, outputIndex) {
1320
+ const aScalar = utils.bytesToNumberLE(privViewKey);
1321
+ const R = ExtPoint$2.fromHex(txPubKey);
1322
+ // Monero derivation: 8*a*R (cofactor multiplication)
1323
+ const aR = R.multiply(aScalar).multiply(BigInt(8));
1324
+ // Hs(8*a*R || i)
1325
+ const keyData = deriveKeyData(aR, outputIndex);
1326
+ const hsBytes = hashToScalar(keyData);
1327
+ const hs = utils.bytesToNumberLE(hsBytes);
1328
+ // x = Hs(8*a*R || i) + b
1329
+ const b = utils.bytesToNumberLE(privSpendKey);
1330
+ const L = BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989');
1331
+ const x = (hs + b) % L;
1332
+ return utils.numberToBytesLE(x, 32);
1333
+ }
1334
+ /**
1335
+ * Check if an output belongs to us.
1336
+ * Computes P' = Hs(a*R || i)*G + B and checks if P' == P.
1337
+ *
1338
+ * @param txPubKey - Transaction public key R (32 bytes)
1339
+ * @param privViewKey - Our private view key a (32 bytes)
1340
+ * @param pubSpendKey - Our public spend key B (32 bytes)
1341
+ * @param outputKey - The output's public key P (32 bytes)
1342
+ * @param outputIndex - Output index i
1343
+ * @returns true if the output belongs to us
1344
+ */
1345
+ function isOutputForUs(txPubKey, privViewKey, pubSpendKey, outputKey, outputIndex) {
1346
+ const aScalar = utils.bytesToNumberLE(privViewKey);
1347
+ const R = ExtPoint$2.fromHex(txPubKey);
1348
+ const B = ExtPoint$2.fromHex(pubSpendKey);
1349
+ // Monero derivation: 8*a*R (cofactor multiplication)
1350
+ const aR = R.multiply(aScalar).multiply(BigInt(8));
1351
+ const keyData = deriveKeyData(aR, outputIndex);
1352
+ const hsBytes = hashToScalar(keyData);
1353
+ const hs = utils.bytesToNumberLE(hsBytes);
1354
+ const expectedP = ExtPoint$2.BASE.multiply(hs).add(B);
1355
+ const actualP = ExtPoint$2.fromHex(outputKey);
1356
+ return expectedP.equals(actualP);
1357
+ }
1358
+
1359
+ /**
1360
+ * ECDH-based amount encryption/decryption for Monero RingCT.
1361
+ *
1362
+ * Shared secret: ss = 8 * r * A (or 8 * a * R)
1363
+ * Amount mask: first 8 bytes of keccak256("amount" || Hs(ss || index))
1364
+ * Encrypted amount: XOR of 8-byte LE amount with mask
1365
+ */
1366
+ const ExtPoint$1 = ed25519.ed25519.ExtendedPoint;
1367
+ function encodeVarint(n) {
1368
+ const bytes = [];
1369
+ while (n >= 0x80) {
1370
+ bytes.push((n & 0x7f) | 0x80);
1371
+ n >>>= 7;
1372
+ }
1373
+ bytes.push(n & 0x7f);
1374
+ return new Uint8Array(bytes);
1375
+ }
1376
+ /**
1377
+ * Derive the shared secret scalar for amount encryption.
1378
+ * Hs(r*A || i) — same derivation as stealth address.
1379
+ */
1380
+ function derivationToScalar(sharedSecret, outputIndex) {
1381
+ const indexBytes = encodeVarint(outputIndex);
1382
+ const combined = new Uint8Array(sharedSecret.length + indexBytes.length);
1383
+ combined.set(sharedSecret, 0);
1384
+ combined.set(indexBytes, sharedSecret.length);
1385
+ return scReduce32(sha3.keccak_256(combined));
1386
+ }
1387
+ /**
1388
+ * Generate the amount mask for XOR encryption.
1389
+ * mask = keccak256("amount" || scalar)[0..8]
1390
+ */
1391
+ function amountMask(scalar) {
1392
+ const prefix = new TextEncoder().encode('amount');
1393
+ const data = new Uint8Array(prefix.length + scalar.length);
1394
+ data.set(prefix, 0);
1395
+ data.set(scalar, prefix.length);
1396
+ return sha3.keccak_256(data).slice(0, 8);
1397
+ }
1398
+ /**
1399
+ * Derive ECDH shared secret point from tx private key and recipient's public view key.
1400
+ * Returns the compressed point bytes of r*A.
1401
+ */
1402
+ function deriveSharedSecret(txPrivKey, pubViewKey) {
1403
+ const rScalar = utils.bytesToNumberLE(txPrivKey);
1404
+ const A = ExtPoint$1.fromHex(pubViewKey);
1405
+ // Monero derivation: 8*r*A (cofactor multiplication)
1406
+ return A.multiply(rScalar).multiply(BigInt(8)).toRawBytes();
1407
+ }
1408
+ /**
1409
+ * Compute the 1-byte view tag for an output.
1410
+ * view_tag = keccak256("view_tag" || derivation || varint(outputIndex))[0]
1411
+ */
1412
+ function computeViewTag(sharedSecret, outputIndex) {
1413
+ const prefix = new TextEncoder().encode('view_tag');
1414
+ const idxBuf = encodeVarint(outputIndex);
1415
+ const data = new Uint8Array(prefix.length + sharedSecret.length + idxBuf.length);
1416
+ data.set(prefix, 0);
1417
+ data.set(sharedSecret, prefix.length);
1418
+ data.set(idxBuf, prefix.length + sharedSecret.length);
1419
+ return sha3.keccak_256(data)[0];
1420
+ }
1421
+ /**
1422
+ * Encrypt an amount using ECDH-derived mask.
1423
+ * @param amount - Amount in piconero (bigint)
1424
+ * @param sharedSecret - ECDH shared secret (r*A compressed bytes)
1425
+ * @param outputIndex - Output index for key derivation
1426
+ * @returns 8-byte encrypted amount
1427
+ */
1428
+ function encryptAmount(amount, sharedSecret, outputIndex) {
1429
+ const scalar = derivationToScalar(sharedSecret, outputIndex);
1430
+ const mask = amountMask(scalar);
1431
+ const amountBytes = utils.numberToBytesLE(amount, 8);
1432
+ const encrypted = new Uint8Array(8);
1433
+ for (let i = 0; i < 8; i++) {
1434
+ encrypted[i] = amountBytes[i] ^ mask[i];
1435
+ }
1436
+ return encrypted;
1437
+ }
1438
+ /**
1439
+ * Decrypt an amount using ECDH-derived mask.
1440
+ * @param encrypted - 8-byte encrypted amount
1441
+ * @param sharedSecret - ECDH shared secret (a*R compressed bytes)
1442
+ * @param outputIndex - Output index for key derivation
1443
+ * @returns Decrypted amount in piconero
1444
+ */
1445
+ function decryptAmount(encrypted, sharedSecret, outputIndex) {
1446
+ if (encrypted.length < 8)
1447
+ throw new Error(`decryptAmount: expected 8 bytes, got ${encrypted.length}`);
1448
+ const scalar = derivationToScalar(sharedSecret, outputIndex);
1449
+ const mask = amountMask(scalar);
1450
+ const decrypted = new Uint8Array(8);
1451
+ for (let i = 0; i < 8; i++) {
1452
+ decrypted[i] = encrypted[i] ^ mask[i];
1453
+ }
1454
+ return utils.bytesToNumberLE(decrypted);
1455
+ }
1456
+
1457
+ /**
1458
+ * Decoy output selection for Monero ring signatures.
1459
+ * Uses gamma distribution (shape=19.28, scale=1/1.61) matching Monero's wallet2.
1460
+ *
1461
+ * Reference: monero/src/wallet/wallet2.cpp (get_outs)
1462
+ */
1463
+ const RING_SIZE = 16;
1464
+ const GAMMA_SHAPE = 19.28;
1465
+ const GAMMA_RATE = 1.61; // scale = 1/rate
1466
+ /**
1467
+ * Generate a cryptographically secure random float in [0, 1).
1468
+ */
1469
+ function secureRandom() {
1470
+ const buf = new Uint32Array(1);
1471
+ crypto.getRandomValues(buf);
1472
+ return buf[0] / 0x100000000;
1473
+ }
1474
+ /**
1475
+ * Sample from gamma distribution using Marsaglia & Tsang's method.
1476
+ * Uses crypto.getRandomValues() for privacy-safe randomness.
1477
+ */
1478
+ function sampleGamma(shape, rate) {
1479
+ const d = shape - 1 / 3;
1480
+ const c = 1 / Math.sqrt(9 * d);
1481
+ while (true) {
1482
+ let x;
1483
+ let v;
1484
+ do {
1485
+ // Box-Muller for standard normal
1486
+ const u1 = secureRandom();
1487
+ const u2 = secureRandom();
1488
+ x = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
1489
+ v = Math.pow(1 + c * x, 3);
1490
+ } while (v <= 0);
1491
+ const u = secureRandom();
1492
+ if (u < 1 - 0.0331 * Math.pow(x, 4) || Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
1493
+ return (d * v) / rate;
1494
+ }
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Select decoy global output indices for ring signature construction.
1499
+ *
1500
+ * @param realGlobalIndex - Global output index of the real input
1501
+ * @param numOutputs - Total number of outputs on chain
1502
+ * @param newestHeight - Current blockchain height
1503
+ * @param outputDistribution - Cumulative output distribution per block
1504
+ * @param distStartHeight - Start height of distribution data
1505
+ * @returns Array of RING_SIZE - 1 decoy global indices (excluding the real one)
1506
+ */
1507
+ function selectDecoys(realGlobalIndex, numOutputs, newestHeight, outputDistribution, distStartHeight) {
1508
+ const decoys = new Set();
1509
+ const maxIndex = numOutputs - 1;
1510
+ let attempts = 0;
1511
+ while (decoys.size < RING_SIZE - 1 && attempts < 1000) {
1512
+ attempts++;
1513
+ // Sample from gamma distribution
1514
+ let blockOffset = Math.floor(sampleGamma(GAMMA_SHAPE, GAMMA_RATE));
1515
+ // Map to a block height (counting from the newest block backwards)
1516
+ let blockHeight = newestHeight - blockOffset;
1517
+ // Ensure within valid range
1518
+ if (blockHeight < distStartHeight)
1519
+ blockHeight = distStartHeight;
1520
+ if (blockHeight >= distStartHeight + outputDistribution.length) {
1521
+ blockHeight = distStartHeight + outputDistribution.length - 1;
1522
+ }
1523
+ // Get output range for this block
1524
+ const distIdx = blockHeight - distStartHeight;
1525
+ const blockStart = distIdx > 0 ? outputDistribution[distIdx - 1] : 0;
1526
+ const blockEnd = outputDistribution[distIdx];
1527
+ if (blockEnd <= blockStart)
1528
+ continue; // empty block
1529
+ // Pick random output within this block
1530
+ const outputIdx = blockStart + Math.floor(secureRandom() * (blockEnd - blockStart));
1531
+ if (outputIdx > maxIndex)
1532
+ continue;
1533
+ if (outputIdx === realGlobalIndex)
1534
+ continue;
1535
+ if (decoys.has(outputIdx))
1536
+ continue;
1537
+ decoys.add(outputIdx);
1538
+ }
1539
+ // If we couldn't get enough decoys from gamma sampling, fill randomly
1540
+ let fallbackAttempts = 0;
1541
+ while (decoys.size < RING_SIZE - 1 && fallbackAttempts < 10000) {
1542
+ fallbackAttempts++;
1543
+ const idx = Math.floor(secureRandom() * (maxIndex + 1));
1544
+ if (idx !== realGlobalIndex && !decoys.has(idx)) {
1545
+ decoys.add(idx);
1546
+ }
1547
+ }
1548
+ if (decoys.size < RING_SIZE - 1) {
1549
+ throw new Error(`Insufficient decoys: need ${RING_SIZE - 1}, got ${decoys.size} from ${maxIndex + 1} outputs`);
1550
+ }
1551
+ return Array.from(decoys).sort((a, b) => a - b);
1552
+ }
1553
+ /**
1554
+ * Build sorted ring with real index.
1555
+ * Returns the ring indices (sorted) and the position of the real input.
1556
+ */
1557
+ function buildRingIndices(realGlobalIndex, decoys) {
1558
+ const all = [...decoys, realGlobalIndex].sort((a, b) => a - b);
1559
+ const realIndex = all.indexOf(realGlobalIndex);
1560
+ return { indices: all, realIndex };
1561
+ }
1562
+ /**
1563
+ * Convert absolute global indices to relative offsets (as stored in tx).
1564
+ */
1565
+ function toRelativeOffsets(sortedIndices) {
1566
+ const offsets = [BigInt(sortedIndices[0])];
1567
+ for (let i = 1; i < sortedIndices.length; i++) {
1568
+ offsets.push(BigInt(sortedIndices[i] - sortedIndices[i - 1]));
1569
+ }
1570
+ return offsets;
1571
+ }
1572
+
1573
+ /**
1574
+ * Monero transaction binary serialization.
1575
+ * Reference: monero/src/cryptonote_basic/cryptonote_format_utils.cpp
1576
+ */
1577
+ /**
1578
+ * Encode a number as a Monero varint (7 bits per byte, MSB = continuation flag).
1579
+ */
1580
+ function writeVarint(n) {
1581
+ let val = typeof n === 'number' ? BigInt(n) : n;
1582
+ const bytes = [];
1583
+ while (val >= BigInt(0x80)) {
1584
+ bytes.push(Number(val & BigInt(0x7f)) | 0x80);
1585
+ val >>= BigInt(7);
1586
+ }
1587
+ bytes.push(Number(val & BigInt(0x7f)));
1588
+ return new Uint8Array(bytes);
1589
+ }
1590
+ /**
1591
+ * Helper class for building binary buffers.
1592
+ */
1593
+ class TxWriter {
1594
+ constructor() {
1595
+ this.parts = [];
1596
+ }
1597
+ writeBytes(data) {
1598
+ this.parts.push(data);
1599
+ }
1600
+ writeVarint(n) {
1601
+ this.parts.push(writeVarint(n));
1602
+ }
1603
+ writeKey(key) {
1604
+ if (key.length !== 32)
1605
+ throw new Error(`Expected 32-byte key, got ${key.length}`);
1606
+ this.parts.push(key);
1607
+ }
1608
+ writeByte(b) {
1609
+ this.parts.push(new Uint8Array([b]));
1610
+ }
1611
+ toBytes() {
1612
+ const totalLen = this.parts.reduce((sum, p) => sum + p.length, 0);
1613
+ const result = new Uint8Array(totalLen);
1614
+ let offset = 0;
1615
+ for (const part of this.parts) {
1616
+ result.set(part, offset);
1617
+ offset += part.length;
1618
+ }
1619
+ return result;
1620
+ }
1621
+ }
1622
+ function serializeInput(w, input) {
1623
+ w.writeByte(0x02); // txin_to_key tag
1624
+ w.writeVarint(input.amount);
1625
+ w.writeVarint(input.keyOffsets.length);
1626
+ for (const offset of input.keyOffsets) {
1627
+ w.writeVarint(offset);
1628
+ }
1629
+ w.writeKey(input.keyImage);
1630
+ }
1631
+ function serializeOutput(w, output) {
1632
+ w.writeVarint(output.amount);
1633
+ w.writeByte(0x03); // txout_to_tagged_key variant tag
1634
+ w.writeKey(output.key);
1635
+ w.writeByte(output.viewTag); // 1-byte view tag for fast scanning
1636
+ }
1637
+ /**
1638
+ * Serialize the transaction prefix (everything except RingCT signatures).
1639
+ */
1640
+ function serializeTxPrefix(tx) {
1641
+ const w = new TxWriter();
1642
+ w.writeVarint(tx.version);
1643
+ w.writeVarint(tx.unlockTime);
1644
+ // Inputs
1645
+ w.writeVarint(tx.inputs.length);
1646
+ for (const input of tx.inputs) {
1647
+ serializeInput(w, input);
1648
+ }
1649
+ // Outputs
1650
+ w.writeVarint(tx.outputs.length);
1651
+ for (const output of tx.outputs) {
1652
+ serializeOutput(w, output);
1653
+ }
1654
+ // Extra
1655
+ w.writeVarint(tx.extra.length);
1656
+ w.writeBytes(tx.extra);
1657
+ return w.toBytes();
1658
+ }
1659
+ /**
1660
+ * Compute the transaction prefix hash (keccak256 of serialized prefix).
1661
+ * This is the message signed by CLSAG.
1662
+ */
1663
+ function txPrefixHash(tx) {
1664
+ return sha3.keccak_256(serializeTxPrefix(tx));
1665
+ }
1666
+ /**
1667
+ * Serialize the RCT signature base (type, fee, encrypted amounts, output commitments).
1668
+ */
1669
+ function serializeRctBase(rct) {
1670
+ const w = new TxWriter();
1671
+ w.writeByte(rct.type);
1672
+ w.writeVarint(rct.txnFee);
1673
+ // Encrypted amounts (8 bytes each for v2)
1674
+ for (const ecdh of rct.ecdhInfo) {
1675
+ w.writeBytes(ecdh);
1676
+ }
1677
+ // Output commitments
1678
+ for (const outPk of rct.outPk) {
1679
+ w.writeKey(outPk);
1680
+ }
1681
+ return w.toBytes();
1682
+ }
1683
+ /**
1684
+ * Serialize a CLSAG signature.
1685
+ */
1686
+ function serializeClsag(sig) {
1687
+ const w = new TxWriter();
1688
+ // s scalars
1689
+ for (const s of sig.s) {
1690
+ w.writeKey(s);
1691
+ }
1692
+ // c1 challenge
1693
+ w.writeKey(sig.c1);
1694
+ // D (commitment key image, already scaled by 1/8)
1695
+ w.writeKey(sig.D);
1696
+ return w.toBytes();
1697
+ }
1698
+ /**
1699
+ * Serialize a Bulletproofs+ proof.
1700
+ */
1701
+ function serializeBPPlus(proof) {
1702
+ const w = new TxWriter();
1703
+ w.writeKey(proof.A);
1704
+ w.writeKey(proof.A1);
1705
+ w.writeKey(proof.B);
1706
+ w.writeKey(proof.r1);
1707
+ w.writeKey(proof.s1);
1708
+ w.writeKey(proof.d1);
1709
+ // L and R vectors
1710
+ w.writeVarint(proof.L.length);
1711
+ for (const l of proof.L) {
1712
+ w.writeKey(l);
1713
+ }
1714
+ for (const r of proof.R) {
1715
+ w.writeKey(r);
1716
+ }
1717
+ return w.toBytes();
1718
+ }
1719
+ /**
1720
+ * Serialize the prunable RCT data (pseudo-outputs, CLSAGs, BP+ proofs).
1721
+ */
1722
+ function serializeRctPrunable(rct) {
1723
+ const w = new TxWriter();
1724
+ // BP+ proofs count + data
1725
+ w.writeVarint(rct.bppProofs.length);
1726
+ for (const proof of rct.bppProofs) {
1727
+ w.writeBytes(serializeBPPlus(proof));
1728
+ }
1729
+ // CLSAGs
1730
+ for (const clsag of rct.clsags) {
1731
+ w.writeBytes(serializeClsag(clsag));
1732
+ }
1733
+ // Pseudo-outputs
1734
+ for (const po of rct.pseudoOuts) {
1735
+ w.writeKey(po);
1736
+ }
1737
+ return w.toBytes();
1738
+ }
1739
+ /**
1740
+ * Compute the RCT signature hash (message for CLSAG signing).
1741
+ * hash = keccak256(prefixHash || keccak256(rctBase) || keccak256(rctPrunable))
1742
+ */
1743
+ function rctSigHash(prefixHash, rctBase, rctPrunable) {
1744
+ const combined = new Uint8Array(96);
1745
+ combined.set(prefixHash, 0);
1746
+ combined.set(sha3.keccak_256(rctBase), 32);
1747
+ combined.set(sha3.keccak_256(rctPrunable), 64);
1748
+ return sha3.keccak_256(combined);
1749
+ }
1750
+ /**
1751
+ * Serialize a complete transaction.
1752
+ */
1753
+ function serializeTransaction(tx) {
1754
+ const w = new TxWriter();
1755
+ w.writeBytes(serializeTxPrefix(tx));
1756
+ w.writeBytes(serializeRctBase(tx.rctSignatures));
1757
+ w.writeBytes(serializeRctPrunable(tx.rctSignatures));
1758
+ return w.toBytes();
1759
+ }
1760
+
1761
+ /**
1762
+ * Monero transaction builder.
1763
+ * Orchestrates: stealth addresses, Pedersen commitments, ECDH encryption,
1764
+ * CLSAG signatures, Bulletproofs+ range proofs, and serialization.
1765
+ *
1766
+ * Reference: monero/src/wallet/wallet2.cpp (create_transactions_2)
1767
+ */
1768
+ const L = BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989');
1769
+ function randomScalar() {
1770
+ const bytes = new Uint8Array(32);
1771
+ crypto.getRandomValues(bytes);
1772
+ return scReduce32(bytes);
1773
+ }
1774
+ /**
1775
+ * Build a complete Monero RingCT transaction.
1776
+ *
1777
+ * @param inputs - Spendable outputs to use as inputs
1778
+ * @param destinations - Recipients + amounts
1779
+ * @param changeDestination - Change address keys (pubView, pubSpend)
1780
+ * @param fee - Transaction fee in piconero
1781
+ * @param privViewKey - Sender's private view key
1782
+ * @param privSpendKey - Sender's private spend key
1783
+ * @param daemonUrl - Daemon RPC URL for decoy selection
1784
+ * @returns Built transaction ready for broadcast
1785
+ */
1786
+ function buildTransaction(inputs, destinations, changeDestination, fee, privViewKey, privSpendKey, daemonUrl) {
1787
+ return __awaiter(this, void 0, void 0, function* () {
1788
+ if (inputs.length === 0)
1789
+ throw new Error('No inputs');
1790
+ if (destinations.length === 0)
1791
+ throw new Error('No destinations');
1792
+ const totalIn = inputs.reduce((sum, inp) => sum + inp.amount, BigInt(0));
1793
+ const totalOut = destinations.reduce((sum, d) => sum + d.amount, BigInt(0));
1794
+ if (totalIn < totalOut + fee)
1795
+ throw new Error('Insufficient funds');
1796
+ // Add change output if needed
1797
+ const change = totalIn - totalOut - fee;
1798
+ const allDestinations = [...destinations];
1799
+ if (change > BigInt(0)) {
1800
+ allDestinations.push({
1801
+ pubViewKey: changeDestination.pubViewKey,
1802
+ pubSpendKey: changeDestination.pubSpendKey,
1803
+ amount: change,
1804
+ });
1805
+ }
1806
+ // Generate tx private key
1807
+ const txPrivKey = randomScalar();
1808
+ const txPubKey = secretKeyToPublicKey(txPrivKey);
1809
+ // --- Build outputs ---
1810
+ const txOutputs = [];
1811
+ const outAmounts = [];
1812
+ const outMasks = [];
1813
+ const outCommitments = [];
1814
+ const ecdhInfo = [];
1815
+ for (let i = 0; i < allDestinations.length; i++) {
1816
+ const dest = allDestinations[i];
1817
+ // ECDH shared secret for this output
1818
+ const sharedSecret = deriveSharedSecret(txPrivKey, dest.pubViewKey);
1819
+ // One-time output key
1820
+ const outputKey = deriveOutputKey(txPrivKey, dest.pubViewKey, dest.pubSpendKey, i);
1821
+ const viewTag = computeViewTag(sharedSecret, i);
1822
+ txOutputs.push({ amount: BigInt(0), key: outputKey, viewTag });
1823
+ // Output commitment mask
1824
+ const outMask = randomScalar();
1825
+ outMasks.push(outMask);
1826
+ outAmounts.push(dest.amount);
1827
+ // Pedersen commitment: C = mask*G + amount*H
1828
+ const commitment = commit(outMask, dest.amount);
1829
+ outCommitments.push(commitment.toRawBytes());
1830
+ // ECDH encrypted amount
1831
+ const encrypted = encryptAmount(dest.amount, sharedSecret, i);
1832
+ ecdhInfo.push(encrypted);
1833
+ }
1834
+ // --- Build inputs with decoys ---
1835
+ const distData = yield getOutputDistribution(daemonUrl);
1836
+ const height = yield getHeight(daemonUrl);
1837
+ const numOutputs = distData.distribution[distData.distribution.length - 1];
1838
+ const txInputs = [];
1839
+ const rings = [];
1840
+ const realIndices = [];
1841
+ const inputPrivKeys = [];
1842
+ const inputMasks = [];
1843
+ const keyImages = [];
1844
+ const pseudoOuts = [];
1845
+ const pseudoMasks = [];
1846
+ for (let i = 0; i < inputs.length; i++) {
1847
+ const inp = inputs[i];
1848
+ // Derive the one-time private key for this input
1849
+ const inputPrivKey = deriveInputKey(inp.txPubKey, privViewKey, privSpendKey, inp.outputIndex);
1850
+ inputPrivKeys.push(inputPrivKey);
1851
+ // Key image
1852
+ const ki = generateKeyImage(inputPrivKey, inp.publicKey);
1853
+ keyImages.push(ki);
1854
+ // Select decoys
1855
+ const decoys = selectDecoys(inp.globalIndex, numOutputs, height, distData.distribution, distData.startHeight);
1856
+ const { indices: ringIndices, realIndex } = buildRingIndices(inp.globalIndex, decoys);
1857
+ realIndices.push(realIndex);
1858
+ const offsets = toRelativeOffsets(ringIndices);
1859
+ // Fetch ring member public keys and commitments from daemon
1860
+ const outsReq = ringIndices.map((idx) => ({ amount: 0, index: idx }));
1861
+ const outs = yield getOuts(daemonUrl, outsReq);
1862
+ const ring = outs.map((out) => ({
1863
+ dest: hexToBytes(out.key),
1864
+ mask: hexToBytes(out.mask),
1865
+ }));
1866
+ rings.push(ring);
1867
+ txInputs.push({
1868
+ amount: BigInt(0),
1869
+ keyOffsets: offsets,
1870
+ keyImage: ki,
1871
+ });
1872
+ inputMasks.push(inp.mask);
1873
+ // Pseudo-output commitment (random mask, same amount as real input)
1874
+ const pseudoMask = randomScalar();
1875
+ pseudoMasks.push(pseudoMask);
1876
+ const pseudoCommitment = commit(pseudoMask, inp.amount);
1877
+ pseudoOuts.push(pseudoCommitment.toRawBytes());
1878
+ }
1879
+ // Adjust last pseudo-output mask so commitments balance:
1880
+ // sum(pseudoMasks) = sum(outMasks) + 0 (fee mask is 0)
1881
+ // So: pseudoMasks[last] = sum(outMasks) - sum(pseudoMasks[0..n-2])
1882
+ if (inputs.length > 0) {
1883
+ let sumOutMasks = BigInt(0);
1884
+ for (const m of outMasks)
1885
+ sumOutMasks = (sumOutMasks + utils.bytesToNumberLE(m)) % L;
1886
+ let sumPseudoMasks = BigInt(0);
1887
+ for (let i = 0; i < pseudoMasks.length - 1; i++) {
1888
+ sumPseudoMasks = (sumPseudoMasks + utils.bytesToNumberLE(pseudoMasks[i])) % L;
1889
+ }
1890
+ let lastMask = (sumOutMasks - sumPseudoMasks) % L;
1891
+ if (lastMask < BigInt(0))
1892
+ lastMask += L;
1893
+ const lastIdx = pseudoMasks.length - 1;
1894
+ pseudoMasks[lastIdx] = new Uint8Array(utils.numberToBytesLE(lastMask, 32));
1895
+ // Recompute last pseudo-output commitment
1896
+ pseudoOuts[lastIdx] = commit(pseudoMasks[lastIdx], inputs[lastIdx].amount).toRawBytes();
1897
+ }
1898
+ // --- Bulletproofs+ range proof ---
1899
+ const bppProof = bulletproofPlusProve(outAmounts, outMasks);
1900
+ // --- Build tx extra (tx public key) ---
1901
+ const extra = concatBytes(new Uint8Array([0x01]), txPubKey); // tag 0x01 = tx pubkey
1902
+ // --- Assemble transaction ---
1903
+ const rctSig = {
1904
+ type: 6, // RCTTypeBulletproofPlus
1905
+ txnFee: fee,
1906
+ ecdhInfo,
1907
+ outPk: outCommitments,
1908
+ pseudoOuts,
1909
+ clsags: [], // filled below
1910
+ bppProofs: [bppProof],
1911
+ };
1912
+ const tx = {
1913
+ version: 2,
1914
+ unlockTime: BigInt(0),
1915
+ inputs: txInputs,
1916
+ outputs: txOutputs,
1917
+ extra,
1918
+ rctSignatures: rctSig,
1919
+ };
1920
+ // --- Sign each input with CLSAG ---
1921
+ const prefixHash = txPrefixHash(tx);
1922
+ const rctBase = serializeRctBase(rctSig);
1923
+ const rctPrunable = serializeRctPrunable(rctSig);
1924
+ const sigMessage = rctSigHash(prefixHash, rctBase, rctPrunable);
1925
+ for (let i = 0; i < inputs.length; i++) {
1926
+ // z = inputMask - pseudoMask
1927
+ let z = (utils.bytesToNumberLE(inputMasks[i]) - utils.bytesToNumberLE(pseudoMasks[i])) % L;
1928
+ if (z < BigInt(0))
1929
+ z += L;
1930
+ const zBytes = new Uint8Array(utils.numberToBytesLE(z, 32));
1931
+ const sig = clsagSign(sigMessage, rings[i], pseudoOuts[i], inputPrivKeys[i], zBytes, realIndices[i], keyImages[i]);
1932
+ tx.rctSignatures.clsags.push(sig);
1933
+ }
1934
+ // Serialize
1935
+ const txBytes = serializeTransaction(tx);
1936
+ const txHex = bytesToHex(txBytes);
1937
+ // Monero txid = keccak256(prefixHash || keccak256(rctBase) || keccak256(rctPrunable))
1938
+ // Must recompute rctPrunable after CLSAGs are filled in
1939
+ const finalRctPrunable = serializeRctPrunable(rctSig);
1940
+ const txHash = bytesToHex(rctSigHash(prefixHash, rctBase, finalRctPrunable));
1941
+ return { tx, txHex, txHash };
1942
+ });
1943
+ }
1944
+
1945
+ /**
1946
+ * Daemon-based blockchain scanner for Monero.
1947
+ * Scans blocks using the view key to find owned outputs and detect spent outputs.
1948
+ * Used as fallback when no LWS (Light Wallet Server) is available.
1949
+ *
1950
+ * This is intentionally slow — scanning the full chain from genesis is impractical.
1951
+ * Use a restore height close to when the wallet was created.
1952
+ */
1953
+ const ExtPoint = ed25519.ed25519.ExtendedPoint;
1954
+ /**
1955
+ * Parse tx public key from the extra field of a decoded Monero transaction.
1956
+ * The extra field is an array of bytes; tag 0x01 followed by 32 bytes is the tx pub key.
1957
+ */
1958
+ function parseTxPubKey(extraBytes) {
1959
+ for (let i = 0; i < extraBytes.length; i++) {
1960
+ if (extraBytes[i] === 0x01 && i + 32 < extraBytes.length) {
1961
+ return new Uint8Array(extraBytes.slice(i + 1, i + 33));
1962
+ }
1963
+ // Tag 0x02 = nonce, skip its length
1964
+ if (extraBytes[i] === 0x02 && i + 1 < extraBytes.length) {
1965
+ i += extraBytes[i + 1] + 1;
1966
+ }
1967
+ }
1968
+ return null;
1969
+ }
1970
+ /**
1971
+ * Compute the ECDH shared secret for amount decryption: a*R (view key * tx pub key).
1972
+ * Returns compressed point bytes.
1973
+ */
1974
+ function computeSharedSecret(privViewKey, txPubKey) {
1975
+ const a = utils.bytesToNumberLE(privViewKey);
1976
+ const R = ExtPoint.fromHex(txPubKey);
1977
+ // Monero's generate_key_derivation: derivation = 8 * a * R (cofactor multiplication)
1978
+ return R.multiply(a).multiply(BigInt(8)).toRawBytes();
1979
+ }
1980
+ /**
1981
+ * Process decoded transactions to find owned outputs and spent key images.
1982
+ */
1983
+ function processTransactions(decodedTxs, privViewKey, pubSpendKey, privSpendKey, heightTimestampMap) {
1984
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1985
+ const ownedOutputs = [];
1986
+ const spentKeyImages = new Set();
1987
+ for (const txEntry of decodedTxs) {
1988
+ let txJson;
1989
+ try {
1990
+ txJson = JSON.parse(txEntry.as_json);
1991
+ }
1992
+ catch (_j) {
1993
+ continue;
1994
+ }
1995
+ // Collect spent key images from inputs
1996
+ if (txJson.vin) {
1997
+ for (const input of txJson.vin) {
1998
+ const ki = ((_a = input.key) === null || _a === void 0 ? void 0 : _a.key_image) || ((_b = input.key) === null || _b === void 0 ? void 0 : _b.k_image);
1999
+ if (ki)
2000
+ spentKeyImages.add(ki);
2001
+ }
2002
+ }
2003
+ if (!txJson.extra || !txJson.vout)
2004
+ continue;
2005
+ const txPubKey = parseTxPubKey(txJson.extra);
2006
+ if (!txPubKey)
2007
+ continue;
2008
+ const sharedSecret = computeSharedSecret(privViewKey, txPubKey);
2009
+ const meta = (_c = heightTimestampMap.get(txEntry.tx_hash)) !== null && _c !== void 0 ? _c : {
2010
+ height: txEntry.block_height,
2011
+ timestamp: txEntry.block_timestamp,
2012
+ };
2013
+ for (let i = 0; i < txJson.vout.length; i++) {
2014
+ const out = txJson.vout[i];
2015
+ const outputKeyHex = (_e = (_d = out.target.tagged_key) === null || _d === void 0 ? void 0 : _d.key) !== null && _e !== void 0 ? _e : out.target.key;
2016
+ if (!outputKeyHex)
2017
+ continue;
2018
+ // View tag optimization: skip EC math if view tag doesn't match
2019
+ const viewTag = (_f = out.target.tagged_key) === null || _f === void 0 ? void 0 : _f.view_tag;
2020
+ if (viewTag !== undefined) {
2021
+ const expected = computeViewTag(sharedSecret, i);
2022
+ if (expected !== parseInt(viewTag, 16))
2023
+ continue;
2024
+ }
2025
+ const outputKey = hexToBytes(outputKeyHex);
2026
+ try {
2027
+ if (!isOutputForUs(txPubKey, privViewKey, pubSpendKey, outputKey, i))
2028
+ continue;
2029
+ }
2030
+ catch (_k) {
2031
+ continue;
2032
+ }
2033
+ // Decrypt amount from RingCT ecdhInfo
2034
+ let amount = BigInt(0);
2035
+ if ((_h = (_g = txJson.rct_signatures) === null || _g === void 0 ? void 0 : _g.ecdhInfo) === null || _h === void 0 ? void 0 : _h[i]) {
2036
+ const encryptedHex = txJson.rct_signatures.ecdhInfo[i].amount;
2037
+ const encrypted = hexToBytes(encryptedHex);
2038
+ try {
2039
+ amount = decryptAmount(encrypted, sharedSecret, i);
2040
+ }
2041
+ catch (_l) {
2042
+ continue;
2043
+ }
2044
+ }
2045
+ // Compute key image for spent detection
2046
+ const oneTimePrivKey = deriveInputKey(txPubKey, privViewKey, privSpendKey, i);
2047
+ const keyImage = bytesToHex(generateKeyImage(oneTimePrivKey, outputKey));
2048
+ ownedOutputs.push({
2049
+ txHash: txEntry.tx_hash,
2050
+ outputIndex: i,
2051
+ amount,
2052
+ globalIndex: -1, // Not available from get_transactions; outputs from daemon scan cannot be spent directly
2053
+ publicKey: outputKey,
2054
+ txPubKey,
2055
+ keyImage,
2056
+ height: meta.height,
2057
+ timestamp: meta.timestamp,
2058
+ });
2059
+ }
2060
+ }
2061
+ return { ownedOutputs, spentKeyImages };
2062
+ }
2063
+ /**
2064
+ * Scan a range of blocks for owned outputs.
2065
+ * Uses batched RPC calls: fetches block tx hashes in parallel, then
2066
+ * fetches all transactions in a single get_transactions call per batch.
2067
+ */
2068
+ function scanBlocks(daemonUrl, privViewKey, pubSpendKey, privSpendKey, fromHeight, toHeight, onProgress) {
2069
+ return __awaiter(this, void 0, void 0, function* () {
2070
+ const allOwnedOutputs = [];
2071
+ const allSpentKeyImages = new Set();
2072
+ // Step 1: Fetch all block headers in one RPC call to find blocks with txs
2073
+ const HEADER_BATCH = 500;
2074
+ const blocksWithTxs = [];
2075
+ for (let h = fromHeight; h <= toHeight; h += HEADER_BATCH) {
2076
+ const rangeEnd = Math.min(h + HEADER_BATCH - 1, toHeight);
2077
+ try {
2078
+ const headers = yield getBlockHeadersRange(daemonUrl, h, rangeEnd);
2079
+ for (const hdr of headers) {
2080
+ if (hdr.num_txes > 0) {
2081
+ blocksWithTxs.push({ height: hdr.height, timestamp: hdr.timestamp });
2082
+ }
2083
+ }
2084
+ }
2085
+ catch (err) {
2086
+ console.error(`[XMR scanner] getBlockHeadersRange failed for ${h}-${rangeEnd}:`, err.message);
2087
+ }
2088
+ }
2089
+ console.log(`[XMR scanner] ${blocksWithTxs.length} blocks with transactions out of ${toHeight - fromHeight + 1} total`);
2090
+ // Step 2: For each block with txs, fetch tx hashes sequentially
2091
+ const allTxHashes = [];
2092
+ const heightMap = new Map();
2093
+ for (const block of blocksWithTxs) {
2094
+ try {
2095
+ const result = yield getBlockTxHashes(daemonUrl, block.height);
2096
+ for (const txHash of result.txHashes) {
2097
+ allTxHashes.push(txHash);
2098
+ heightMap.set(txHash, { height: block.height, timestamp: block.timestamp });
2099
+ }
2100
+ }
2101
+ catch (err) {
2102
+ console.warn(`[XMR scanner] getBlockTxHashes failed for height ${block.height}:`, err.message);
2103
+ }
2104
+ }
2105
+ console.log(`[XMR scanner] collected ${allTxHashes.length} tx hashes from ${blocksWithTxs.length} blocks`);
2106
+ // Step 3: Fetch and process transactions in small batches (process-as-you-go to limit memory)
2107
+ const TX_BATCH = 25;
2108
+ let processedTxs = 0;
2109
+ console.log(`[XMR scanner] processing ${allTxHashes.length} txs in batches of ${TX_BATCH}...`);
2110
+ for (let t = 0; t < allTxHashes.length; t += TX_BATCH) {
2111
+ const slice = allTxHashes.slice(t, t + TX_BATCH);
2112
+ try {
2113
+ const decodedTxs = yield getTransactionsDecoded(daemonUrl, slice);
2114
+ const result = processTransactions(decodedTxs, privViewKey, pubSpendKey, privSpendKey, heightMap);
2115
+ allOwnedOutputs.push(...result.ownedOutputs);
2116
+ for (const ki of result.spentKeyImages)
2117
+ allSpentKeyImages.add(ki);
2118
+ processedTxs += decodedTxs.length;
2119
+ }
2120
+ catch (err) {
2121
+ console.error(`[XMR scanner] getTransactionsDecoded failed (batch ${t}):`, err.message);
2122
+ }
2123
+ if (onProgress) {
2124
+ const progress = Math.min(t + TX_BATCH, allTxHashes.length) / allTxHashes.length;
2125
+ onProgress({
2126
+ currentHeight: fromHeight + Math.floor(progress * (toHeight - fromHeight)),
2127
+ totalHeight: toHeight,
2128
+ outputsFound: allOwnedOutputs.length,
2129
+ });
2130
+ }
2131
+ }
2132
+ console.log(`[XMR scanner] processed ${processedTxs} txs, found ${allOwnedOutputs.length} owned outputs`);
2133
+ return { ownedOutputs: allOwnedOutputs, spentKeyImages: allSpentKeyImages };
2134
+ });
2135
+ }
2136
+ /**
2137
+ * Compute balance from scan results.
2138
+ * Balance = sum of owned outputs whose key images are NOT in the spent set.
2139
+ */
2140
+ function computeBalance(ownedOutputs, spentKeyImages) {
2141
+ let balance = BigInt(0);
2142
+ for (const out of ownedOutputs) {
2143
+ if (!spentKeyImages.has(out.keyImage)) {
2144
+ balance += out.amount;
2145
+ }
2146
+ }
2147
+ return balance;
2148
+ }
2149
+ /**
2150
+ * Get unspent outputs from scan results.
2151
+ */
2152
+ function getUnspentOutputs(ownedOutputs, spentKeyImages) {
2153
+ return ownedOutputs.filter((out) => !spentKeyImages.has(out.keyImage));
2154
+ }
2155
+
2156
+ class Client extends xchainClient.BaseXChainClient {
2157
+ constructor(params = defaultXMRParams) {
2158
+ var _a, _b, _c, _d;
2159
+ super(XMRChain, Object.assign(Object.assign({}, defaultXMRParams), params));
2160
+ this.lwsLoggedIn = false;
2161
+ /** Cached scan state for daemon fallback */
2162
+ this.scanCache = null;
2163
+ this.explorerProviders = (_a = params.explorerProviders) !== null && _a !== void 0 ? _a : defaultXMRParams.explorerProviders;
2164
+ this.daemonUrls = (_b = params.daemonUrls) !== null && _b !== void 0 ? _b : defaultXMRParams.daemonUrls;
2165
+ this.lwsUrls = (_c = params.lwsUrls) !== null && _c !== void 0 ? _c : defaultXMRParams.lwsUrls;
2166
+ this.restoreHeight = (_d = params.restoreHeight) !== null && _d !== void 0 ? _d : 0;
2167
+ }
2168
+ getAssetInfo() {
2169
+ return {
2170
+ asset: AssetXMR,
2171
+ decimal: XMR_DECIMALS,
2172
+ };
2173
+ }
2174
+ getExplorerUrl() {
2175
+ return this.explorerProviders[this.getNetwork()].getExplorerUrl();
2176
+ }
2177
+ getExplorerAddressUrl(address) {
2178
+ return this.explorerProviders[this.getNetwork()].getExplorerAddressUrl(address);
2179
+ }
2180
+ getExplorerTxUrl(txID) {
2181
+ return this.explorerProviders[this.getNetwork()].getExplorerTxUrl(txID);
2182
+ }
2183
+ getFullDerivationPath(walletIndex) {
2184
+ if (!this.rootDerivationPaths) {
2185
+ throw Error('Can not generate derivation path due to root derivation path is undefined');
2186
+ }
2187
+ return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'`;
2188
+ }
2189
+ /**
2190
+ * Get the current address asynchronously.
2191
+ * Derives keys from mnemonic and encodes as Monero address (pure JS, no WASM).
2192
+ */
2193
+ getAddressAsync(index) {
2194
+ return __awaiter(this, void 0, void 0, function* () {
2195
+ const spendKey = this.getPrivateSpendKey(index !== null && index !== void 0 ? index : 0);
2196
+ const keys = deriveKeyPairs(spendKey);
2197
+ const networkType = getMoneroNetworkType(this.getNetwork());
2198
+ return encodeAddress(keys.publicSpendKey, keys.publicViewKey, networkType);
2199
+ });
2200
+ }
2201
+ /**
2202
+ * @deprecated Use getAddressAsync instead
2203
+ */
2204
+ getAddress() {
2205
+ throw Error('Sync method not supported');
2206
+ }
2207
+ validateAddress(address) {
2208
+ return validateMoneroAddress(address);
2209
+ }
2210
+ /**
2211
+ * Get balance via LWS, falling back to daemon scanning if LWS is unavailable.
2212
+ */
2213
+ getBalance(address) {
2214
+ return __awaiter(this, void 0, void 0, function* () {
2215
+ // Try LWS first
2216
+ const urls = this.lwsUrls[this.getNetwork()];
2217
+ if (urls && urls.length > 0) {
2218
+ const viewKeyHex = this.getViewKeyHex(0);
2219
+ for (const url of urls) {
2220
+ try {
2221
+ if (!this.lwsLoggedIn) {
2222
+ yield login(url, address, viewKeyHex);
2223
+ this.lwsLoggedIn = true;
2224
+ }
2225
+ const info = yield getAddressInfo(url, address, viewKeyHex);
2226
+ const received = BigInt(info.total_received);
2227
+ const sent = BigInt(info.total_sent);
2228
+ const balance = received - sent;
2229
+ return [{ asset: AssetXMR, amount: xchainUtil.baseAmount(balance.toString(), XMR_DECIMALS) }];
2230
+ }
2231
+ catch (error) {
2232
+ console.warn(`LWS ${url} failed for getBalance:`, error.message);
2233
+ this.lwsLoggedIn = false;
2234
+ continue;
2235
+ }
2236
+ }
2237
+ }
2238
+ // Fallback: daemon scanning
2239
+ console.warn('LWS unavailable, falling back to daemon scanning (this may be slow)');
2240
+ const scanResult = yield this.daemonScan();
2241
+ const balance = computeBalance(scanResult.ownedOutputs, scanResult.spentKeyImages);
2242
+ return [{ asset: AssetXMR, amount: xchainUtil.baseAmount(balance.toString(), XMR_DECIMALS) }];
2243
+ });
2244
+ }
2245
+ /**
2246
+ * Get transaction fees via daemon RPC.
2247
+ */
2248
+ getFees() {
2249
+ return __awaiter(this, void 0, void 0, function* () {
2250
+ const urls = this.daemonUrls[this.getNetwork()];
2251
+ for (const url of urls) {
2252
+ try {
2253
+ const feeEstimate = yield getFeeEstimate(url);
2254
+ const fee = xchainUtil.baseAmount(feeEstimate.toString(), XMR_DECIMALS);
2255
+ return {
2256
+ type: xchainClient.FeeType.FlatFee,
2257
+ [xchainClient.FeeOption.Average]: fee,
2258
+ [xchainClient.FeeOption.Fast]: fee,
2259
+ [xchainClient.FeeOption.Fastest]: fee,
2260
+ };
2261
+ }
2262
+ catch (error) {
2263
+ console.warn(`Daemon ${url} failed for getFees:`, error.message);
2264
+ continue;
2265
+ }
2266
+ }
2267
+ throw Error('No daemon able to get fees');
2268
+ });
2269
+ }
2270
+ /**
2271
+ * Get transaction details by hash via daemon RPC.
2272
+ */
2273
+ getTransactionData(txId) {
2274
+ return __awaiter(this, void 0, void 0, function* () {
2275
+ const urls = this.daemonUrls[this.getNetwork()];
2276
+ for (const url of urls) {
2277
+ try {
2278
+ const txs = yield getTransactions(url, [txId]);
2279
+ if (!txs.length)
2280
+ throw Error('Transaction not found');
2281
+ const tx = txs[0];
2282
+ return {
2283
+ asset: AssetXMR,
2284
+ date: new Date(tx.block_timestamp * 1000),
2285
+ type: xchainClient.TxType.Transfer,
2286
+ hash: tx.tx_hash,
2287
+ from: [],
2288
+ to: [],
2289
+ };
2290
+ }
2291
+ catch (error) {
2292
+ console.warn(`Daemon ${url} failed for getTransactionData:`, error.message);
2293
+ continue;
2294
+ }
2295
+ }
2296
+ throw Error('No daemon able to get transaction data');
2297
+ });
2298
+ }
2299
+ /**
2300
+ * Get transaction history via LWS, falling back to daemon scanning.
2301
+ * Due to Monero privacy, counterparty addresses are unavailable;
2302
+ * the own address is used with net amount for from/to.
2303
+ */
2304
+ getTransactions(params) {
2305
+ return __awaiter(this, void 0, void 0, function* () {
2306
+ var _a, _b, _c;
2307
+ const address = (params === null || params === void 0 ? void 0 : params.address) || (yield this.getAddressAsync(0));
2308
+ const offset = (_a = params === null || params === void 0 ? void 0 : params.offset) !== null && _a !== void 0 ? _a : 0;
2309
+ const limit = (_b = params === null || params === void 0 ? void 0 : params.limit) !== null && _b !== void 0 ? _b : 10;
2310
+ // Try LWS first
2311
+ const urls = this.lwsUrls[this.getNetwork()];
2312
+ if (urls && urls.length > 0) {
2313
+ const viewKeyHex = this.getViewKeyHex(0);
2314
+ for (const url of urls) {
2315
+ try {
2316
+ if (!this.lwsLoggedIn) {
2317
+ yield login(url, address, viewKeyHex);
2318
+ this.lwsLoggedIn = true;
2319
+ }
2320
+ const result = yield getAddressTxs(url, address, viewKeyHex);
2321
+ const confirmedTxs = ((_c = result.transactions) !== null && _c !== void 0 ? _c : [])
2322
+ .filter((tx) => !tx.mempool && tx.height > 0)
2323
+ .sort((a, b) => b.height - a.height);
2324
+ const paginated = confirmedTxs.slice(offset, offset + limit);
2325
+ const txs = paginated.map((tx) => {
2326
+ const received = BigInt(tx.total_received);
2327
+ const sent = BigInt(tx.total_sent);
2328
+ const netAmount = received - sent;
2329
+ const isIncoming = netAmount > BigInt(0);
2330
+ return {
2331
+ asset: AssetXMR,
2332
+ date: new Date(tx.timestamp),
2333
+ type: xchainClient.TxType.Transfer,
2334
+ hash: tx.hash,
2335
+ from: isIncoming
2336
+ ? []
2337
+ : [{ from: address, amount: xchainUtil.baseAmount((BigInt(-1) * netAmount).toString(), XMR_DECIMALS) }],
2338
+ to: isIncoming ? [{ to: address, amount: xchainUtil.baseAmount(netAmount.toString(), XMR_DECIMALS) }] : [],
2339
+ };
2340
+ });
2341
+ return { total: confirmedTxs.length, txs };
2342
+ }
2343
+ catch (error) {
2344
+ console.warn(`LWS ${url} failed for getTransactions:`, error.message);
2345
+ this.lwsLoggedIn = false;
2346
+ continue;
2347
+ }
2348
+ }
2349
+ }
2350
+ // Fallback: daemon scanning
2351
+ console.warn('LWS unavailable, falling back to daemon scanning for tx history');
2352
+ const scanResult = yield this.daemonScan();
2353
+ const allOutputs = scanResult.ownedOutputs.sort((a, b) => b.height - a.height);
2354
+ const paginated = allOutputs.slice(offset, offset + limit);
2355
+ const txs = paginated.map((out) => ({
2356
+ asset: AssetXMR,
2357
+ date: new Date(out.timestamp * 1000),
2358
+ type: xchainClient.TxType.Transfer,
2359
+ hash: out.txHash,
2360
+ from: [],
2361
+ to: [{ to: address, amount: xchainUtil.baseAmount(out.amount.toString(), XMR_DECIMALS) }],
2362
+ }));
2363
+ return { total: allOutputs.length, txs };
2364
+ });
2365
+ }
2366
+ /**
2367
+ * Transfer XMR to a recipient address.
2368
+ * Builds a complete RingCT transaction with CLSAG signatures and Bulletproofs+ range proof.
2369
+ */
2370
+ transfer(params) {
2371
+ return __awaiter(this, void 0, void 0, function* () {
2372
+ var _a, _b, _c;
2373
+ const { recipient, amount } = params;
2374
+ if (!recipient)
2375
+ throw new Error('Recipient address is required');
2376
+ if (!amount)
2377
+ throw new Error('Amount is required');
2378
+ const recipientKeys = decodeAddress(recipient);
2379
+ const spendKey = this.getPrivateSpendKey((_a = params.walletIndex) !== null && _a !== void 0 ? _a : 0);
2380
+ const keys = deriveKeyPairs(spendKey);
2381
+ // Get daemon URL
2382
+ const daemonUrls = this.daemonUrls[this.getNetwork()];
2383
+ if (!daemonUrls || daemonUrls.length === 0)
2384
+ throw new Error('No daemon URLs configured');
2385
+ const daemonUrl = daemonUrls[0];
2386
+ // Get LWS URL for unspent outputs
2387
+ const lwsUrls = this.lwsUrls[this.getNetwork()];
2388
+ if (!lwsUrls || lwsUrls.length === 0)
2389
+ throw new Error('No LWS URLs configured');
2390
+ const address = yield this.getAddressAsync((_b = params.walletIndex) !== null && _b !== void 0 ? _b : 0);
2391
+ const viewKeyHex = this.getViewKeyHex((_c = params.walletIndex) !== null && _c !== void 0 ? _c : 0);
2392
+ if (!this.lwsLoggedIn) {
2393
+ yield login(lwsUrls[0], address, viewKeyHex);
2394
+ this.lwsLoggedIn = true;
2395
+ }
2396
+ // Get unspent outputs
2397
+ const unspent = yield getUnspentOuts(lwsUrls[0], address, viewKeyHex, amount.amount().toString());
2398
+ // Convert LWS outputs to SpendableOutput format
2399
+ const amountPiconero = BigInt(amount.amount().toString());
2400
+ const fee = BigInt(unspent.per_byte_fee) * BigInt(3000); // approximate tx size * per byte fee
2401
+ // Select inputs to cover amount + fee
2402
+ const spendableOutputs = [];
2403
+ let selectedTotal = BigInt(0);
2404
+ const needed = amountPiconero + fee;
2405
+ for (const out of unspent.outputs) {
2406
+ if (selectedTotal >= needed)
2407
+ break;
2408
+ const outAmount = BigInt(out.amount);
2409
+ // Parse rct field: first 64 chars = commitment, next 64 = mask
2410
+ if (out.rct.length < 128) {
2411
+ console.warn(`Skipping output ${out.global_index}: invalid rct data (length ${out.rct.length})`);
2412
+ continue;
2413
+ }
2414
+ const rctMask = hexToBytes(out.rct.substring(64, 128));
2415
+ spendableOutputs.push({
2416
+ globalIndex: out.global_index,
2417
+ amount: outAmount,
2418
+ mask: rctMask,
2419
+ txPubKey: hexToBytes(out.tx_pub_key),
2420
+ outputIndex: out.index,
2421
+ publicKey: hexToBytes(out.public_key),
2422
+ });
2423
+ selectedTotal += outAmount;
2424
+ }
2425
+ if (selectedTotal < needed) {
2426
+ throw new Error(`Insufficient funds: have ${selectedTotal}, need ${needed}`);
2427
+ }
2428
+ const destinations = [
2429
+ {
2430
+ pubViewKey: recipientKeys.publicViewKey,
2431
+ pubSpendKey: recipientKeys.publicSpendKey,
2432
+ amount: amountPiconero,
2433
+ },
2434
+ ];
2435
+ const changeKeys = {
2436
+ pubViewKey: keys.publicViewKey,
2437
+ pubSpendKey: keys.publicSpendKey,
2438
+ };
2439
+ const built = yield buildTransaction(spendableOutputs, destinations, changeKeys, fee, keys.privateViewKey, spendKey, daemonUrl);
2440
+ // Broadcast
2441
+ yield this.broadcastTx(built.txHex);
2442
+ return built.txHash;
2443
+ });
2444
+ }
2445
+ /**
2446
+ * Broadcast a signed transaction hex to the network.
2447
+ * Note: The returned hash is keccak256 of the raw tx blob, which differs from
2448
+ * the canonical Monero txid (three-hash construction). Use transfer() for the correct txid.
2449
+ */
2450
+ broadcastTx(txHex) {
2451
+ return __awaiter(this, void 0, void 0, function* () {
2452
+ const urls = this.daemonUrls[this.getNetwork()];
2453
+ for (const url of urls) {
2454
+ try {
2455
+ yield sendRawTransaction(url, txHex);
2456
+ const txBytes = hexToBytes(txHex);
2457
+ return bytesToHex(sha3.keccak_256(txBytes));
2458
+ }
2459
+ catch (error) {
2460
+ console.warn(`Daemon ${url} failed for broadcastTx:`, error.message);
2461
+ continue;
2462
+ }
2463
+ }
2464
+ throw Error('No daemon able to broadcast transaction');
2465
+ });
2466
+ }
2467
+ /**
2468
+ * Prepare an unsigned transaction.
2469
+ * Monero transactions require private key context for ring signature
2470
+ * construction, so this method is not supported.
2471
+ */
2472
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2473
+ prepareTx(_params) {
2474
+ return __awaiter(this, void 0, void 0, function* () {
2475
+ throw Error('prepareTx is not supported for Monero. Use transfer() instead.');
2476
+ });
2477
+ }
2478
+ /**
2479
+ * Get the private view key hex for a wallet index.
2480
+ */
2481
+ getViewKeyHex(index) {
2482
+ const spendKey = this.getPrivateSpendKey(index);
2483
+ const keys = deriveKeyPairs(spendKey);
2484
+ return bytesToHex(keys.privateViewKey);
2485
+ }
2486
+ /**
2487
+ * Derives private spend key from BIP-39 mnemonic via SLIP-10 derivation.
2488
+ */
2489
+ getPrivateSpendKey(index) {
2490
+ if (!this.phrase)
2491
+ throw new Error('Phrase must be provided');
2492
+ const seed = xchainCrypto.getSeed(this.phrase);
2493
+ const hd = slip10__default.default.fromMasterSeed(seed);
2494
+ const derivedKey = hd.derive(this.getFullDerivationPath(index)).privateKey;
2495
+ return scReduce32(derivedKey);
2496
+ }
2497
+ /**
2498
+ * Scan the blockchain via daemon RPC to find owned outputs.
2499
+ * Uses cached results and scans incrementally from the last scanned height.
2500
+ */
2501
+ daemonScan() {
2502
+ return __awaiter(this, arguments, void 0, function* (walletIndex = 0) {
2503
+ const daemonUrls = this.daemonUrls[this.getNetwork()];
2504
+ if (!daemonUrls || daemonUrls.length === 0) {
2505
+ throw new Error('No daemon URLs configured');
2506
+ }
2507
+ const daemonUrl = daemonUrls[0];
2508
+ const currentHeight = yield getHeight(daemonUrl);
2509
+ const fromHeight = this.scanCache ? this.scanCache.lastHeight + 1 : this.restoreHeight;
2510
+ if (fromHeight >= currentHeight && this.scanCache) {
2511
+ return this.scanCache;
2512
+ }
2513
+ const spendKey = this.getPrivateSpendKey(walletIndex);
2514
+ const keys = deriveKeyPairs(spendKey);
2515
+ const result = yield scanBlocks(daemonUrl, keys.privateViewKey, keys.publicSpendKey, spendKey, fromHeight, currentHeight - 1);
2516
+ // Merge with cached results
2517
+ const ownedOutputs = this.scanCache ? [...this.scanCache.ownedOutputs, ...result.ownedOutputs] : result.ownedOutputs;
2518
+ const spentKeyImages = this.scanCache
2519
+ ? new Set([...this.scanCache.spentKeyImages, ...result.spentKeyImages])
2520
+ : result.spentKeyImages;
2521
+ this.scanCache = {
2522
+ lastHeight: currentHeight - 1,
2523
+ ownedOutputs,
2524
+ spentKeyImages,
2525
+ };
2526
+ return this.scanCache;
2527
+ });
2528
+ }
2529
+ }
2530
+
2531
+ exports.AssetXMR = AssetXMR;
2532
+ exports.Client = Client;
2533
+ exports.XMRChain = XMRChain;
2534
+ exports.XMR_DECIMALS = XMR_DECIMALS;
2535
+ exports.computeBalance = computeBalance;
2536
+ exports.defaultXMRParams = defaultXMRParams;
2537
+ exports.getUnspentOutputs = getUnspentOutputs;
2538
+ exports.scanBlocks = scanBlocks;