@spirobel/monero-wallet-api 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/api.d.ts +25 -96
- package/dist/api.js +23 -175
- package/dist/io/BunFileInterface.d.ts +32 -0
- package/dist/io/atomicWrite.d.ts +2 -0
- package/dist/io/atomicWrite.js +10 -0
- package/dist/io/extension.d.ts +18 -0
- package/dist/io/extension.js +11 -0
- package/dist/io/indexedDB.d.ts +45 -0
- package/dist/io/indexedDB.js +221 -0
- package/dist/io/readDir.d.ts +1 -0
- package/dist/io/readDir.js +7 -0
- package/dist/io/sleep.d.ts +1 -0
- package/dist/io/sleep.js +1 -0
- package/dist/keypairs-seeds/keypairs.d.ts +29 -0
- package/dist/keypairs-seeds/keypairs.js +207 -0
- package/dist/keypairs-seeds/writeKeypairs.d.ts +12 -0
- package/dist/keypairs-seeds/writeKeypairs.js +78 -0
- package/dist/node-interaction/binaryEndpoints.d.ts +59 -14
- package/dist/node-interaction/binaryEndpoints.js +110 -54
- package/dist/node-interaction/jsonEndpoints.d.ts +249 -187
- package/dist/node-interaction/jsonEndpoints.js +287 -0
- package/dist/node-interaction/nodeUrl.d.ts +129 -0
- package/dist/node-interaction/nodeUrl.js +113 -0
- package/dist/scanning-syncing/backgroundWorker.d.ts +6 -0
- package/dist/scanning-syncing/backgroundWorker.js +56 -0
- package/dist/scanning-syncing/connectionStatus.d.ts +15 -0
- package/dist/scanning-syncing/connectionStatus.js +35 -0
- package/dist/scanning-syncing/openWallet.d.ts +28 -0
- package/dist/scanning-syncing/openWallet.js +57 -0
- package/dist/scanning-syncing/scanSettings.d.ts +96 -0
- package/dist/scanning-syncing/scanSettings.js +240 -0
- package/dist/scanning-syncing/scanresult/computeKeyImage.d.ts +3 -0
- package/dist/scanning-syncing/scanresult/computeKeyImage.js +21 -0
- package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.d.ts +28 -0
- package/dist/scanning-syncing/scanresult/getBlocksbinBuffer.js +52 -0
- package/dist/scanning-syncing/scanresult/reorg.d.ts +14 -0
- package/dist/scanning-syncing/scanresult/reorg.js +78 -0
- package/dist/scanning-syncing/scanresult/scanCache.d.ts +84 -0
- package/dist/scanning-syncing/scanresult/scanCache.js +134 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +149 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +648 -0
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +64 -0
- package/dist/scanning-syncing/scanresult/scanResult.js +213 -0
- package/dist/scanning-syncing/scanresult/scanStats.d.ts +60 -0
- package/dist/scanning-syncing/scanresult/scanStats.js +273 -0
- package/dist/scanning-syncing/worker-entrypoints/worker.d.ts +1 -0
- package/dist/scanning-syncing/worker-entrypoints/worker.js +8 -0
- package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -0
- package/dist/scanning-syncing/worker-mains/worker.js +7 -0
- package/dist/send-functionality/conversion.d.ts +4 -0
- package/dist/send-functionality/conversion.js +75 -0
- package/dist/send-functionality/inputSelection.d.ts +13 -0
- package/dist/send-functionality/inputSelection.js +8 -0
- package/dist/send-functionality/transactionBuilding.d.ts +51 -0
- package/dist/send-functionality/transactionBuilding.js +111 -0
- package/dist/tools/monero-tools.d.ts +46 -0
- package/dist/tools/monero-tools.js +165 -0
- package/dist/viewpair/ViewPair.d.ts +157 -0
- package/dist/viewpair/ViewPair.js +346 -0
- package/dist/wasm-processing/wasi.js +1 -2
- package/dist/wasm-processing/wasmFile.d.ts +1 -1
- package/dist/wasm-processing/wasmFile.js +2 -2
- package/dist/wasm-processing/wasmProcessor.d.ts +16 -4
- package/dist/wasm-processing/wasmProcessor.js +23 -7
- package/package.json +29 -6
- package/dist/testscrap.js +0 -36
- /package/dist/{testscrap.d.ts → io/BunFileInterface.js} +0 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { atomicWrite, NodeUrl, signTransaction, ViewPair, } from "../../api";
|
|
2
|
+
import { prepareInput, sumPayments, } from "../../send-functionality/inputSelection";
|
|
3
|
+
import { createWebworker } from "../backgroundWorker";
|
|
4
|
+
import { spendable } from "./scanResult";
|
|
5
|
+
import { openScanSettingsFile, readPrivateSpendKeyFromEnv, readWalletFromScanSettings, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, writeNodeUrlToScanSettings, writeStartHeightToScanSettings, writeWalletToScanSettings, } from "../scanSettings";
|
|
6
|
+
import { findRange, lastRange, readCacheFileDefaultLocation, writeCacheFileDefaultLocationThrows, } from "./scanCache";
|
|
7
|
+
import { alignScanStatsWithCache, isSelfSpent, processTxlogInputs, processTxlogPayments, writeStatsFileDefaultLocation, } from "./scanStats";
|
|
8
|
+
import { readWriteConnectionStatusFile, } from "../connectionStatus";
|
|
9
|
+
export class ScanCacheOpened {
|
|
10
|
+
view_pair;
|
|
11
|
+
wallet_route;
|
|
12
|
+
no_worker;
|
|
13
|
+
masterCacheChanged;
|
|
14
|
+
_start_height;
|
|
15
|
+
scan_settings_path;
|
|
16
|
+
pathPrefix;
|
|
17
|
+
workerError;
|
|
18
|
+
static async create(params) {
|
|
19
|
+
const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, params.pathPrefix);
|
|
20
|
+
const walletSettings = await readWalletFromScanSettings(params.primary_address, params.scan_settings_path);
|
|
21
|
+
if (!walletSettings)
|
|
22
|
+
throw new Error(`wallet not found in settings. did you call openwallet with the right params?
|
|
23
|
+
Either wrong file name supplied to params.scan_settings_path: ${params.scan_settings_path}
|
|
24
|
+
Or wrong primary_address supplied params.primary_address: ${params.primary_address}`);
|
|
25
|
+
if (!params.primary_address)
|
|
26
|
+
throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
|
|
27
|
+
// read secret_view_key and secret_spend_key from env
|
|
28
|
+
const walletSettingsWithKeys = await walletSettingsPlusKeys(walletSettings);
|
|
29
|
+
// create viewpair + ScanCacheOpened instance
|
|
30
|
+
const scanCacheOpen = new ScanCacheOpened(await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, walletSettings.node_url), walletSettings.wallet_route, params.no_worker || false, params.masterCacheChanged || null, walletSettings.start_height, params.scan_settings_path, params.pathPrefix, params.workerError);
|
|
31
|
+
if (theCatchToBeOpened)
|
|
32
|
+
scanCacheOpen._cache = theCatchToBeOpened;
|
|
33
|
+
if (!walletSettings.halted) {
|
|
34
|
+
// run webworker (respecting halted param + setting)
|
|
35
|
+
// unpause will start scanning from this.wallet_scan_settings.start_height
|
|
36
|
+
await scanCacheOpen.unpause();
|
|
37
|
+
}
|
|
38
|
+
scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, params.pathPrefix, walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
|
|
39
|
+
return scanCacheOpen;
|
|
40
|
+
}
|
|
41
|
+
get start_height() {
|
|
42
|
+
return this._start_height;
|
|
43
|
+
}
|
|
44
|
+
get current_height() {
|
|
45
|
+
let current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
|
|
46
|
+
return current_range?.end || null;
|
|
47
|
+
}
|
|
48
|
+
get current_top_range_height() {
|
|
49
|
+
if (typeof this._stats === "undefined" || this._stats === null)
|
|
50
|
+
return null;
|
|
51
|
+
return this._stats.height;
|
|
52
|
+
}
|
|
53
|
+
async changeStartHeight(start_height) {
|
|
54
|
+
if (this.worker) {
|
|
55
|
+
this.worker.terminate();
|
|
56
|
+
delete this.worker;
|
|
57
|
+
}
|
|
58
|
+
await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
|
|
59
|
+
this._start_height = start_height;
|
|
60
|
+
await this.unpause();
|
|
61
|
+
}
|
|
62
|
+
get cache() {
|
|
63
|
+
return this._cache;
|
|
64
|
+
}
|
|
65
|
+
get prepending_txs() {
|
|
66
|
+
const txs = [];
|
|
67
|
+
for (const txlog of this._cache.tx_logs || []) {
|
|
68
|
+
if (!txlog ||
|
|
69
|
+
!txlog.sendResult ||
|
|
70
|
+
(txlog.sendResult && txlog.sendResult.status !== "OK"))
|
|
71
|
+
continue;
|
|
72
|
+
const { inputSum, alreadyRecognizedAsSpend } = processTxlogInputs(txlog, this._cache);
|
|
73
|
+
if (alreadyRecognizedAsSpend)
|
|
74
|
+
continue;
|
|
75
|
+
const outWardPaymentSum = processTxlogPayments(txlog, this._cache);
|
|
76
|
+
const self_spent = isSelfSpent(txlog.payments[0].address, this._cache);
|
|
77
|
+
const destination_address = txlog.payments[0].address;
|
|
78
|
+
const inputs = [];
|
|
79
|
+
for (const inputId of txlog.inputs_index) {
|
|
80
|
+
const input = this._cache.outputs[inputId];
|
|
81
|
+
inputs.push(input);
|
|
82
|
+
}
|
|
83
|
+
const typical_fee = 1000000000n; // 0.001 XMR
|
|
84
|
+
const amount = -outWardPaymentSum - typical_fee;
|
|
85
|
+
const prepending_tx = {
|
|
86
|
+
amount,
|
|
87
|
+
txlog,
|
|
88
|
+
inputSum,
|
|
89
|
+
outWardPaymentSum,
|
|
90
|
+
self_spent,
|
|
91
|
+
destination_address,
|
|
92
|
+
inputs,
|
|
93
|
+
};
|
|
94
|
+
txs.push(prepending_tx);
|
|
95
|
+
}
|
|
96
|
+
return txs;
|
|
97
|
+
}
|
|
98
|
+
get transactions() {
|
|
99
|
+
if (typeof this._stats === "undefined" || this._stats === null)
|
|
100
|
+
return [];
|
|
101
|
+
const transactions = [];
|
|
102
|
+
for (const tx of this._stats?.ordered_transactions) {
|
|
103
|
+
transactions.push(this._stats.found_transactions[tx]);
|
|
104
|
+
}
|
|
105
|
+
return transactions;
|
|
106
|
+
}
|
|
107
|
+
get primary_address() {
|
|
108
|
+
return this.view_pair.primary_address;
|
|
109
|
+
}
|
|
110
|
+
get node_url() {
|
|
111
|
+
return this.view_pair.node_url;
|
|
112
|
+
}
|
|
113
|
+
set node_url(nu) {
|
|
114
|
+
this.view_pair.node_url = nu;
|
|
115
|
+
}
|
|
116
|
+
async changeNodeUrlAndStartHeight(node_url, start_height) {
|
|
117
|
+
if (this.worker) {
|
|
118
|
+
this.worker.terminate();
|
|
119
|
+
delete this.worker;
|
|
120
|
+
}
|
|
121
|
+
if (node_url !== undefined) {
|
|
122
|
+
await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
|
|
123
|
+
this.node_url = node_url;
|
|
124
|
+
}
|
|
125
|
+
if (start_height !== undefined) {
|
|
126
|
+
await writeStartHeightToScanSettings(start_height, this.scan_settings_path);
|
|
127
|
+
this._start_height = start_height;
|
|
128
|
+
}
|
|
129
|
+
await this.unpause();
|
|
130
|
+
}
|
|
131
|
+
async changeNodeUrl(node_url) {
|
|
132
|
+
if (this.worker) {
|
|
133
|
+
this.worker.terminate();
|
|
134
|
+
delete this.worker;
|
|
135
|
+
}
|
|
136
|
+
await writeNodeUrlToScanSettings(node_url, this.scan_settings_path);
|
|
137
|
+
this.node_url = node_url;
|
|
138
|
+
await this.unpause();
|
|
139
|
+
}
|
|
140
|
+
async retry() {
|
|
141
|
+
if (this.worker) {
|
|
142
|
+
this.worker.terminate();
|
|
143
|
+
delete this.worker;
|
|
144
|
+
}
|
|
145
|
+
const scan_settings = await readWalletFromScanSettings(this.primary_address, this.scan_settings_path).catch(() => false);
|
|
146
|
+
if (scan_settings) {
|
|
147
|
+
// if there is no scan settings file,
|
|
148
|
+
// the retry loop is stopped.
|
|
149
|
+
// the wallet reset happens through deleting all the scan setting + cache files
|
|
150
|
+
// we want any background retry loops to stop in this case
|
|
151
|
+
//TODO ? write connection status retry
|
|
152
|
+
await this.unpause();
|
|
153
|
+
}
|
|
154
|
+
return scan_settings ? true : false;
|
|
155
|
+
}
|
|
156
|
+
async sendTransaction(signedTx) {
|
|
157
|
+
const node = await NodeUrl.create(this.node_url);
|
|
158
|
+
return await node.sendRawTransaction(signedTx);
|
|
159
|
+
}
|
|
160
|
+
async signTransaction(unsignedTx) {
|
|
161
|
+
const privateSpendKey = readPrivateSpendKeyFromEnv(this._cache.primary_address);
|
|
162
|
+
if (!privateSpendKey)
|
|
163
|
+
throw new Error("privateSpendKey not found in env");
|
|
164
|
+
return await signTransaction(unsignedTx, privateSpendKey);
|
|
165
|
+
}
|
|
166
|
+
async calculateFeeAndSelectInputs(params) {
|
|
167
|
+
const sum = sumPayments(params.payments);
|
|
168
|
+
const node = await NodeUrl.create(this.node_url);
|
|
169
|
+
// 1. get fee estimate
|
|
170
|
+
const feeEstimate = await node.getFeeEstimate();
|
|
171
|
+
const feePerByte = BigInt(feeEstimate.fees[0]);
|
|
172
|
+
if (!params.no_fee_circuit_breaker) {
|
|
173
|
+
// default is false / undefined -> use fee circuit breaker
|
|
174
|
+
const max_plausible_fee = 20000000000n; // 0.02 XMR
|
|
175
|
+
const feeFor10kb = feePerByte * 10000n;
|
|
176
|
+
//2. check if fee is too high
|
|
177
|
+
if (feeFor10kb > max_plausible_fee) {
|
|
178
|
+
throw new Error(`fee too high:
|
|
179
|
+
${feeFor10kb} (fee for 10kb tx size) > ${max_plausible_fee} (0.001 XMR)
|
|
180
|
+
most likely your node is faulty. connect to another node.
|
|
181
|
+
preferably run one yourself locally.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 3. select inputs TODO: log inputs indices
|
|
185
|
+
const selectedInputs = params.inputs || this.selectInputs(sum, feePerByte);
|
|
186
|
+
if (!selectedInputs.length)
|
|
187
|
+
throw new Error("not enough funds");
|
|
188
|
+
return { selectedInputs, feeEstimate };
|
|
189
|
+
}
|
|
190
|
+
async makeTransactionFromSelectedInputs(payments, selectedInputs, feeEstimate) {
|
|
191
|
+
// 4. get output distribution
|
|
192
|
+
const node = await NodeUrl.create(this.node_url);
|
|
193
|
+
const distibution = await node.getOutputDistribution();
|
|
194
|
+
const preparedInputs = [];
|
|
195
|
+
for (const input of selectedInputs) {
|
|
196
|
+
// 5. sample decoys & get outs from node: here is where a privacy compromising event could happen
|
|
197
|
+
preparedInputs.push(prepareInput(node, distibution, input));
|
|
198
|
+
}
|
|
199
|
+
const inputs = [];
|
|
200
|
+
for (const preparedInput of preparedInputs) {
|
|
201
|
+
// 6. make input: combine output with sampled and verified unlocked decoys
|
|
202
|
+
const input = node.makeInput(preparedInput.input, preparedInput.sample.candidates, await preparedInput.outsResponse);
|
|
203
|
+
inputs.push(input);
|
|
204
|
+
}
|
|
205
|
+
// 7. make transaction: combine inputs, payments + fee info
|
|
206
|
+
const unsignedTx = this.view_pair.makeTransaction({
|
|
207
|
+
inputs,
|
|
208
|
+
payments,
|
|
209
|
+
fee_response: feeEstimate,
|
|
210
|
+
fee_priority: "unimportant",
|
|
211
|
+
});
|
|
212
|
+
return unsignedTx;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* this function returns the unsigned transaction, throws {@link SendError}
|
|
216
|
+
*/
|
|
217
|
+
async makeTransaction(params) {
|
|
218
|
+
const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
|
|
219
|
+
return await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
|
|
220
|
+
}
|
|
221
|
+
get daemon_height() {
|
|
222
|
+
return this._cache.daemon_height;
|
|
223
|
+
}
|
|
224
|
+
get amount() {
|
|
225
|
+
return this._stats?.total_spendable_amount || 0n;
|
|
226
|
+
}
|
|
227
|
+
get pending_amount() {
|
|
228
|
+
return this._stats?.total_pending_amount || 0n;
|
|
229
|
+
}
|
|
230
|
+
get subaddresses() {
|
|
231
|
+
return Object.values(this._stats?.subaddresses || {});
|
|
232
|
+
}
|
|
233
|
+
get tx_logs() {
|
|
234
|
+
return this._cache.tx_logs || [];
|
|
235
|
+
}
|
|
236
|
+
async makeSignSendTransaction(params) {
|
|
237
|
+
let maybeInputs = [];
|
|
238
|
+
let maybeFeeEstimate;
|
|
239
|
+
let maybeSendResult;
|
|
240
|
+
try {
|
|
241
|
+
const { selectedInputs, feeEstimate } = await this.calculateFeeAndSelectInputs(params);
|
|
242
|
+
maybeInputs = selectedInputs;
|
|
243
|
+
maybeFeeEstimate = feeEstimate;
|
|
244
|
+
const unsignedTx = await this.makeTransactionFromSelectedInputs(params.payments, selectedInputs, feeEstimate);
|
|
245
|
+
const signedTx = await this.signTransaction(unsignedTx);
|
|
246
|
+
const sendResult = await this.sendTransaction(signedTx);
|
|
247
|
+
maybeSendResult = sendResult;
|
|
248
|
+
if (sendResult.status !== "OK")
|
|
249
|
+
throw new Error("send raw transaction rpc returned error");
|
|
250
|
+
// before writing the scan cache, we stop the worker to avoid a race
|
|
251
|
+
if (this.worker) {
|
|
252
|
+
this.worker.terminate();
|
|
253
|
+
delete this.worker;
|
|
254
|
+
}
|
|
255
|
+
// write txlog to cache + update pending_spent_utxos (affects stats + spendability)
|
|
256
|
+
await writeCacheFileDefaultLocationThrows({
|
|
257
|
+
primary_address: this.primary_address,
|
|
258
|
+
pathPrefix: this.pathPrefix,
|
|
259
|
+
writeCallback: async (cache) => {
|
|
260
|
+
if (!cache.tx_logs)
|
|
261
|
+
cache.tx_logs = [];
|
|
262
|
+
if (!cache.pending_spent_utxos)
|
|
263
|
+
cache.pending_spent_utxos = {};
|
|
264
|
+
const inputs_index = selectedInputs.map((input) => String(input.index_on_blockchain));
|
|
265
|
+
const txLog = {
|
|
266
|
+
sendResult,
|
|
267
|
+
feeEstimate,
|
|
268
|
+
payments: params.payments,
|
|
269
|
+
node_url: this.node_url,
|
|
270
|
+
inputs_index,
|
|
271
|
+
height: this.current_height,
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
};
|
|
274
|
+
const newLen = cache.tx_logs.push(txLog);
|
|
275
|
+
const txLogIndex = newLen - 1;
|
|
276
|
+
for (const inputId of inputs_index) {
|
|
277
|
+
cache.pending_spent_utxos[inputId] = txLogIndex;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
|
|
282
|
+
if (!newCache)
|
|
283
|
+
throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
|
|
284
|
+
const changed_outputs = selectedInputs.map((input) => ({
|
|
285
|
+
change_reason: "spent",
|
|
286
|
+
output: input,
|
|
287
|
+
}));
|
|
288
|
+
await this.feed({
|
|
289
|
+
newCache,
|
|
290
|
+
changed_outputs,
|
|
291
|
+
});
|
|
292
|
+
// restart the worker
|
|
293
|
+
await this.unpause();
|
|
294
|
+
return sendResult;
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
// before writing the scan cache, we stop the worker to avoid a race
|
|
298
|
+
if (this.worker) {
|
|
299
|
+
this.worker.terminate();
|
|
300
|
+
delete this.worker;
|
|
301
|
+
}
|
|
302
|
+
// write txlog error to cache
|
|
303
|
+
await writeCacheFileDefaultLocationThrows({
|
|
304
|
+
primary_address: this.primary_address,
|
|
305
|
+
pathPrefix: this.pathPrefix,
|
|
306
|
+
writeCallback: async (cache) => {
|
|
307
|
+
if (!cache.tx_logs)
|
|
308
|
+
cache.tx_logs = [];
|
|
309
|
+
if (!cache.pending_spent_utxos)
|
|
310
|
+
cache.pending_spent_utxos = {};
|
|
311
|
+
const inputs_index = maybeInputs.map((input) => String(input.index_on_blockchain));
|
|
312
|
+
const txLog = {
|
|
313
|
+
sendResult: maybeSendResult,
|
|
314
|
+
error: String(e || "unknown error"),
|
|
315
|
+
feeEstimate: maybeFeeEstimate,
|
|
316
|
+
payments: params.payments,
|
|
317
|
+
node_url: this.node_url,
|
|
318
|
+
inputs_index,
|
|
319
|
+
height: this.current_height,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
};
|
|
322
|
+
const newLen = cache.tx_logs.push(txLog);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const newCache = await readCacheFileDefaultLocation(this.primary_address, this.pathPrefix);
|
|
326
|
+
if (!newCache)
|
|
327
|
+
throw new Error(`cache not found for primary address: ${this.primary_address}, and path prefix: ${this.pathPrefix}`);
|
|
328
|
+
const changed_outputs = maybeInputs.map((input) => ({
|
|
329
|
+
change_reason: "spent",
|
|
330
|
+
output: input,
|
|
331
|
+
}));
|
|
332
|
+
await this.feed({
|
|
333
|
+
newCache,
|
|
334
|
+
changed_outputs,
|
|
335
|
+
});
|
|
336
|
+
// restart the worker
|
|
337
|
+
await this.unpause();
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* makeStandardTransaction
|
|
343
|
+
*/
|
|
344
|
+
makeStandardTransaction(destination_address, amount) {
|
|
345
|
+
return this.makeTransaction({
|
|
346
|
+
payments: [{ address: destination_address, amount }],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* makeIntegratedAddress
|
|
351
|
+
*/
|
|
352
|
+
makeIntegratedAddress(paymentId) {
|
|
353
|
+
return this.view_pair.makeIntegratedAddress(paymentId);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* This method makes a Subaddress for the Address of the Viewpair it was opened with.
|
|
357
|
+
* The network (mainnet, stagenet, testnet) is the same as the one of the Viewpairaddress.
|
|
358
|
+
* will increment minor by 1 on major 0 in "ScanSettings.json" subaddresses definition
|
|
359
|
+
*
|
|
360
|
+
* if there is an active scan going on, call this here on ScanCacheOpened, so the new subaddress will be scanned
|
|
361
|
+
* (and not on a viewpair / scancacheopened instance that is not conducting the scan, aka where no_worker is true)
|
|
362
|
+
*
|
|
363
|
+
* @returns Adressstring
|
|
364
|
+
*/
|
|
365
|
+
async makeSubaddress() {
|
|
366
|
+
const walletSettings = await readWalletFromScanSettings(this.view_pair.primary_address, this.scan_settings_path);
|
|
367
|
+
if (!walletSettings)
|
|
368
|
+
throw new Error(`wallet not found in settings. did you call openwallet with the right params?
|
|
369
|
+
Either wrong file name supplied to params.scan_settings_path: ${this.scan_settings_path}
|
|
370
|
+
Or wrong primary_address supplied params.primary_address: ${this.view_pair.primary_address}`);
|
|
371
|
+
const last_subaddress_index = walletSettings.subaddress_index || 0;
|
|
372
|
+
const minor = last_subaddress_index + 1;
|
|
373
|
+
const subaddress = this.view_pair.makeSubaddress(minor);
|
|
374
|
+
await writeWalletToScanSettings({
|
|
375
|
+
primary_address: this.view_pair.primary_address,
|
|
376
|
+
subaddress_index: minor,
|
|
377
|
+
scan_settings_path: this.scan_settings_path,
|
|
378
|
+
});
|
|
379
|
+
const created_at_height = lastRange(this._cache.scanned_ranges)?.end || 0;
|
|
380
|
+
const created_at_timestamp = new Date().getTime();
|
|
381
|
+
const new_subaddress = {
|
|
382
|
+
minor,
|
|
383
|
+
address: subaddress,
|
|
384
|
+
created_at_height,
|
|
385
|
+
created_at_timestamp,
|
|
386
|
+
not_yet_included: true,
|
|
387
|
+
};
|
|
388
|
+
this._stats = await writeStatsFileDefaultLocation({
|
|
389
|
+
primary_address: this.primary_address,
|
|
390
|
+
pathPrefix: this.pathPrefix,
|
|
391
|
+
writeCallback: async (stats) => {
|
|
392
|
+
stats.subaddresses[minor.toString()] = new_subaddress;
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
return new_subaddress;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* notify
|
|
399
|
+
*
|
|
400
|
+
* ChangeReason = "added" | "ownspend" | "reorged" | "burned";
|
|
401
|
+
*/
|
|
402
|
+
//TODO PAUSE NOTIFY listner and node status / connection error
|
|
403
|
+
notify(callback) {
|
|
404
|
+
this.notifyListeners.push(callback);
|
|
405
|
+
const id = this.notifyListeners.length - 1;
|
|
406
|
+
return {
|
|
407
|
+
remove: () => (this.notifyListeners[id] = null),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async pause() {
|
|
411
|
+
if (this.worker)
|
|
412
|
+
this.worker.terminate();
|
|
413
|
+
return await writeWalletToScanSettings({
|
|
414
|
+
primary_address: this.view_pair.primary_address,
|
|
415
|
+
halted: true,
|
|
416
|
+
scan_settings_path: this.scan_settings_path,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
stopWorker() {
|
|
420
|
+
if (this.worker) {
|
|
421
|
+
this.worker.terminate();
|
|
422
|
+
delete this.worker;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async unpause() {
|
|
426
|
+
// if worker does not exist yet, start it (if we are not slave / no_worker)
|
|
427
|
+
if (!this.worker && !this.no_worker) {
|
|
428
|
+
this.worker = createWebworker(async (params) => await this.feed(params), this.scan_settings_path, this.pathPrefix, (error) => {
|
|
429
|
+
const workerErrCB = this.workerError;
|
|
430
|
+
readWriteConnectionStatusFile((cs) => {
|
|
431
|
+
if (cs?.last_packet.status === "catastrophic_reorg")
|
|
432
|
+
return;
|
|
433
|
+
const connectionStatus = {
|
|
434
|
+
last_packet: {
|
|
435
|
+
status: "connection_failed",
|
|
436
|
+
bytes_read: 0,
|
|
437
|
+
node_url: this.node_url,
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
return connectionStatus;
|
|
442
|
+
}).then(() => {
|
|
443
|
+
if (workerErrCB)
|
|
444
|
+
workerErrCB(error);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return await writeWalletToScanSettings({
|
|
449
|
+
primary_address: this.view_pair.primary_address,
|
|
450
|
+
halted: false,
|
|
451
|
+
scan_settings_path: this.scan_settings_path,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* selectInputs returns array of inputs, whose sum is larger than amount
|
|
456
|
+
* adds approximate fee for 10kb transaction to amount if feePerByte is supplied
|
|
457
|
+
*/
|
|
458
|
+
selectInputs(amount, feePerByte) {
|
|
459
|
+
if (feePerByte)
|
|
460
|
+
amount += feePerByte * 10000n; // 10kb * feePerByte; for sweeping low amounts inputs[] supplied directly
|
|
461
|
+
const oneInputIsEnough = this.selectOneInput(amount);
|
|
462
|
+
if (oneInputIsEnough)
|
|
463
|
+
return [oneInputIsEnough];
|
|
464
|
+
return this.selectMultipleInputs(amount);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* selectOneInput larger than amount, (smallest one matching this amount)
|
|
468
|
+
*/
|
|
469
|
+
selectOneInput(amount) {
|
|
470
|
+
return this.spendableInputs()
|
|
471
|
+
.filter((output) => output.amount >= amount)
|
|
472
|
+
.sort((a, b) => b.amount > a.amount ? -1 : b.amount < a.amount ? 1 : 0)[0];
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* selectMultipleInputs larger than amount, sorted from largest to smallest until total reaches amount
|
|
476
|
+
*/
|
|
477
|
+
selectMultipleInputs(amount) {
|
|
478
|
+
const selected = [];
|
|
479
|
+
let total = 0n;
|
|
480
|
+
for (const output of this.spendableInputs()) {
|
|
481
|
+
selected.push(output);
|
|
482
|
+
total += output.amount;
|
|
483
|
+
if (total >= amount)
|
|
484
|
+
return selected;
|
|
485
|
+
}
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* get spendableInputs
|
|
490
|
+
*/
|
|
491
|
+
spendableInputs() {
|
|
492
|
+
return Object.values(this._cache.outputs)
|
|
493
|
+
.filter((output) => spendable(output, this._cache, this.current_height || 0))
|
|
494
|
+
.sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0));
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* feed the ScanCacheOpened with new ScanCache as syncing happens
|
|
498
|
+
* if primary_address does not match, do not feed
|
|
499
|
+
* if masterCacheChanged is set, it will be called here
|
|
500
|
+
* for all primary addresses
|
|
501
|
+
*/
|
|
502
|
+
async feed(params) {
|
|
503
|
+
//TODO update aggregated amount stats + height
|
|
504
|
+
if (this.masterCacheChanged)
|
|
505
|
+
this.masterCacheChanged(params);
|
|
506
|
+
if (this.view_pair.primary_address !== params.newCache.primary_address)
|
|
507
|
+
return;
|
|
508
|
+
this._cache = params.newCache;
|
|
509
|
+
this._stats = await alignScanStatsWithCache(this._cache, this.view_pair, this.primary_address, this.pathPrefix, undefined, lastRange(this._cache.scanned_ranges)?.end);
|
|
510
|
+
for (const listener of this.notifyListeners) {
|
|
511
|
+
if (listener)
|
|
512
|
+
listener(params);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
_cache = {
|
|
516
|
+
daemon_height: 0,
|
|
517
|
+
outputs: {},
|
|
518
|
+
own_key_images: {},
|
|
519
|
+
scanned_ranges: [],
|
|
520
|
+
primary_address: "",
|
|
521
|
+
};
|
|
522
|
+
worker = undefined;
|
|
523
|
+
constructor(view_pair, wallet_route, no_worker, masterCacheChanged, _start_height, scan_settings_path, pathPrefix, workerError) {
|
|
524
|
+
this.view_pair = view_pair;
|
|
525
|
+
this.wallet_route = wallet_route;
|
|
526
|
+
this.no_worker = no_worker;
|
|
527
|
+
this.masterCacheChanged = masterCacheChanged;
|
|
528
|
+
this._start_height = _start_height;
|
|
529
|
+
this.scan_settings_path = scan_settings_path;
|
|
530
|
+
this.pathPrefix = pathPrefix;
|
|
531
|
+
this.workerError = workerError;
|
|
532
|
+
}
|
|
533
|
+
_stats = null;
|
|
534
|
+
notifyListeners = [];
|
|
535
|
+
}
|
|
536
|
+
export class ManyScanCachesOpened {
|
|
537
|
+
wallets;
|
|
538
|
+
get start_height() {
|
|
539
|
+
if (this.wallets.length === 0)
|
|
540
|
+
return null;
|
|
541
|
+
return this.wallets[0]?.start_height;
|
|
542
|
+
}
|
|
543
|
+
get current_height() {
|
|
544
|
+
if (this.wallets.length === 0)
|
|
545
|
+
return null;
|
|
546
|
+
return this.wallets[0]?.current_height;
|
|
547
|
+
}
|
|
548
|
+
get node_url() {
|
|
549
|
+
if (this.wallets.length === 0)
|
|
550
|
+
return "";
|
|
551
|
+
return this.wallets[0]?.node_url;
|
|
552
|
+
}
|
|
553
|
+
async changeNodeUrlAndStartHeight(node_url, start_height) {
|
|
554
|
+
if (this.wallets.length === 0)
|
|
555
|
+
throw new Error("no wallets");
|
|
556
|
+
const masterWallet = this.wallets[0];
|
|
557
|
+
return await masterWallet.changeNodeUrlAndStartHeight(node_url, start_height);
|
|
558
|
+
}
|
|
559
|
+
async retry() {
|
|
560
|
+
if (this.wallets.length === 0)
|
|
561
|
+
throw new Error("no wallets");
|
|
562
|
+
const masterWallet = this.wallets[0];
|
|
563
|
+
return await masterWallet.retry();
|
|
564
|
+
}
|
|
565
|
+
stopWorker() {
|
|
566
|
+
if (this.wallets.length === 0)
|
|
567
|
+
throw new Error("no wallets");
|
|
568
|
+
const masterWallet = this.wallets[0];
|
|
569
|
+
return masterWallet.stopWorker();
|
|
570
|
+
}
|
|
571
|
+
async changeNodeUrl(node_url) {
|
|
572
|
+
if (this.wallets.length === 0)
|
|
573
|
+
throw new Error("no wallets");
|
|
574
|
+
const masterWallet = this.wallets[0];
|
|
575
|
+
return await masterWallet.changeNodeUrl(node_url);
|
|
576
|
+
}
|
|
577
|
+
async changeStartHeight(start_height) {
|
|
578
|
+
if (this.wallets.length === 0)
|
|
579
|
+
throw new Error("no wallets");
|
|
580
|
+
const masterWallet = this.wallets[0];
|
|
581
|
+
return await masterWallet.changeStartHeight(start_height);
|
|
582
|
+
}
|
|
583
|
+
static async create({ scan_settings_path, pathPrefix, no_worker, notifyMasterChanged, no_stats, workerError, }) {
|
|
584
|
+
const scan_settings = await openScanSettingsFile(scan_settings_path);
|
|
585
|
+
if (!scan_settings?.wallets)
|
|
586
|
+
throw new Error(`no wallets in settings file. Did you supply the right path?
|
|
587
|
+
are there wallets in the default '${SCAN_SETTINGS_STORE_NAME_DEFAULT}' file?`);
|
|
588
|
+
const nonHaltedWallets = scan_settings.wallets.filter((wallet) => !wallet?.halted);
|
|
589
|
+
if (!nonHaltedWallets.length)
|
|
590
|
+
return undefined;
|
|
591
|
+
const openedWallets = [];
|
|
592
|
+
const firstNonHaltedWallet = nonHaltedWallets[0];
|
|
593
|
+
if (nonHaltedWallets.length > 1) {
|
|
594
|
+
const slaveWallets = [];
|
|
595
|
+
for (const wallet of nonHaltedWallets.slice(1)) {
|
|
596
|
+
if (!wallet || wallet.halted)
|
|
597
|
+
continue;
|
|
598
|
+
const slaveWallet = await ScanCacheOpened.create({
|
|
599
|
+
...wallet,
|
|
600
|
+
no_worker: true, // slaves depend on master worker
|
|
601
|
+
scan_settings_path,
|
|
602
|
+
pathPrefix,
|
|
603
|
+
no_stats,
|
|
604
|
+
});
|
|
605
|
+
slaveWallets.push(slaveWallet);
|
|
606
|
+
}
|
|
607
|
+
const masterWallet = await ScanCacheOpened.create({
|
|
608
|
+
...firstNonHaltedWallet,
|
|
609
|
+
masterCacheChanged: async (params) => {
|
|
610
|
+
notifyMasterChanged?.(params);
|
|
611
|
+
for (const slave of slaveWallets) {
|
|
612
|
+
await slave.feed(params);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
scan_settings_path,
|
|
616
|
+
pathPrefix,
|
|
617
|
+
no_stats,
|
|
618
|
+
no_worker, // pass no_worker, if you want to manually feed()
|
|
619
|
+
workerError,
|
|
620
|
+
});
|
|
621
|
+
openedWallets.push(masterWallet, ...slaveWallets);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const onlyWallet = await ScanCacheOpened.create({
|
|
625
|
+
masterCacheChanged: async (params) => {
|
|
626
|
+
notifyMasterChanged?.(params);
|
|
627
|
+
},
|
|
628
|
+
...firstNonHaltedWallet,
|
|
629
|
+
scan_settings_path,
|
|
630
|
+
pathPrefix,
|
|
631
|
+
no_stats,
|
|
632
|
+
no_worker, // pass no_worker, if you want to manually feed()
|
|
633
|
+
workerError,
|
|
634
|
+
});
|
|
635
|
+
openedWallets.push(onlyWallet);
|
|
636
|
+
}
|
|
637
|
+
return new ManyScanCachesOpened(openedWallets);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* feed the master wallet and therefore all wallets
|
|
641
|
+
*/
|
|
642
|
+
async feed(params) {
|
|
643
|
+
await this.wallets[0].feed(params);
|
|
644
|
+
}
|
|
645
|
+
constructor(wallets) {
|
|
646
|
+
this.wallets = wallets;
|
|
647
|
+
}
|
|
648
|
+
}
|