@spirobel/monero-wallet-api 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/actionlog/actionLog.d.ts +4 -0
  2. package/dist/actionlog/actionLog.js +9 -0
  3. package/dist/actionlog/actionLogOpened.d.ts +1 -0
  4. package/dist/actionlog/actionLogOpened.js +1 -0
  5. package/dist/api.d.ts +3 -2
  6. package/dist/api.js +3 -2
  7. package/dist/scanning-syncing/backgroundWorker.js +1 -1
  8. package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +29 -3
  9. package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +6 -3
  10. package/dist/scanning-syncing/connectionStatus.d.ts +1 -4
  11. package/dist/scanning-syncing/connectionStatus.js +1 -31
  12. package/dist/scanning-syncing/connectionStatusOpened.d.ts +15 -0
  13. package/dist/scanning-syncing/connectionStatusOpened.js +51 -0
  14. package/dist/scanning-syncing/openWallet.js +1 -1
  15. package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +55 -7
  16. package/dist/scanning-syncing/scanresult/scanCacheOpened.js +191 -50
  17. package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +1 -1
  18. package/dist/scanning-syncing/scanresult/scanCoordination.js +64 -2
  19. package/dist/scanning-syncing/scansettings/scanSettings.d.ts +165 -0
  20. package/dist/scanning-syncing/scansettings/scanSettings.js +338 -0
  21. package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
  22. package/dist/scanning-syncing/worker-mains/worker.js +1 -2
  23. package/dist/scansettings/scanSettings.d.ts +165 -0
  24. package/dist/scansettings/scanSettings.js +338 -0
  25. package/dist/scansettings/scanSettingsOpened.d.ts +127 -0
  26. package/dist/scansettings/scanSettingsOpened.js +475 -0
  27. package/dist/viewpair/ViewPair.js +1 -1
  28. package/package.json +1 -1
@@ -0,0 +1,475 @@
1
+ import { LOCAL_NODE_DEFAULT_URL } from "../node-interaction/nodeUrl";
2
+ import { LOGGING_FUNCTIONS, } from "../io/logging";
3
+ import { getPathPrefix, makeSpendKeyFromSeed, makeViewKey, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, writeEnvLineToDotEnvRefresh, writeScanSettings, writeViewKeyToDotEnv, } from "../api";
4
+ export class ScanSettingsOpened {
5
+ _scan_settings_path;
6
+ _pathPrefix;
7
+ /**
8
+ * open the settings file and return a new ScanSettingsOpened instance.
9
+ * if the file does not exist, creates one with defaults.
10
+ */
11
+ static async create(scan_settings_path = SCAN_SETTINGS_STORE_NAME_DEFAULT, pathPrefix) {
12
+ pathPrefix = getPathPrefix(scan_settings_path, pathPrefix);
13
+ let settings = await openScanSettingsFile(scan_settings_path);
14
+ if (!settings) {
15
+ settings = {
16
+ wallets: [],
17
+ node_url: LOCAL_NODE_DEFAULT_URL,
18
+ start_height: null,
19
+ };
20
+ // persist the defaults so the file exists on disk
21
+ await writeScanSettings(settings, scan_settings_path);
22
+ }
23
+ ScanSettingsOpened._validateLogOptions(settings.logs_include, settings.logs_exclude);
24
+ return new ScanSettingsOpened(settings, scan_settings_path, pathPrefix);
25
+ }
26
+ get node_url() {
27
+ return this._settings.node_url;
28
+ }
29
+ get start_height() {
30
+ return this._settings.start_height;
31
+ }
32
+ get merchant_confirmations() {
33
+ return this._settings.merchant_confirmations;
34
+ }
35
+ get cpu_worker_count() {
36
+ return this._settings.cpu_worker_count;
37
+ }
38
+ get logs() {
39
+ return this._settings.logs;
40
+ }
41
+ get logs_include() {
42
+ return this._settings.logs_include;
43
+ }
44
+ get logs_exclude() {
45
+ return this._settings.logs_exclude;
46
+ }
47
+ get wallets() {
48
+ return this._settings.wallets;
49
+ }
50
+ get scan_settings_path() {
51
+ return this._scan_settings_path;
52
+ }
53
+ get pathPrefix() {
54
+ return this._pathPrefix;
55
+ }
56
+ async setNodeUrl(node_url) {
57
+ await this.reload();
58
+ this._settings.node_url = ScanSettingsOpened._validateNodeUrl(node_url);
59
+ await this._persist();
60
+ }
61
+ async setStartHeight(start_height) {
62
+ await this.reload();
63
+ ScanSettingsOpened._validateStartHeight(start_height);
64
+ this._settings.start_height = start_height;
65
+ await this._persist();
66
+ }
67
+ async setMerchantConfirmations(merchant_confirmations) {
68
+ await this.reload();
69
+ if (merchant_confirmations === undefined ||
70
+ merchant_confirmations === null) {
71
+ delete this._settings.merchant_confirmations;
72
+ }
73
+ else {
74
+ ScanSettingsOpened._validateMerchantConfirmations(merchant_confirmations);
75
+ this._settings.merchant_confirmations = merchant_confirmations;
76
+ }
77
+ await this._persist();
78
+ }
79
+ async setCpuWorkerCount(cpu_worker_count) {
80
+ await this.reload();
81
+ if (cpu_worker_count === null || cpu_worker_count === undefined) {
82
+ delete this._settings.cpu_worker_count;
83
+ }
84
+ else {
85
+ ScanSettingsOpened._validateCpuWorkerCount(cpu_worker_count);
86
+ this._settings.cpu_worker_count = cpu_worker_count;
87
+ }
88
+ await this._persist();
89
+ }
90
+ async setLogSettings(logs, logs_include, logs_exclude) {
91
+ await this.reload();
92
+ if (logs === null) {
93
+ delete this._settings.logs;
94
+ }
95
+ else {
96
+ this._settings.logs = logs;
97
+ }
98
+ if (logs_include === null) {
99
+ delete this._settings.logs_include;
100
+ }
101
+ else {
102
+ this._settings.logs_include = logs_include;
103
+ }
104
+ if (logs_exclude === null) {
105
+ delete this._settings.logs_exclude;
106
+ }
107
+ else {
108
+ this._settings.logs_exclude = logs_exclude;
109
+ }
110
+ // validate before persisting
111
+ ScanSettingsOpened._validateLogOptions(this._settings.logs_include, this._settings.logs_exclude);
112
+ await this._persist();
113
+ }
114
+ /**
115
+ * get a wallet's settings (without env keys).
116
+ * returns undefined if not found.
117
+ */
118
+ getWallet(primary_address) {
119
+ return this._settings.wallets.find((w) => w.primary_address === primary_address);
120
+ }
121
+ /**
122
+ * get a wallet's settings merged with env keys.
123
+ * throws if not found or view key missing.
124
+ */
125
+ async getWalletOpened(primary_address) {
126
+ const wallet = this.getWallet(primary_address);
127
+ if (!wallet)
128
+ throw new Error(`wallet not found: ${primary_address} in ${this._scan_settings_path}`);
129
+ return await walletSettingsPlusKeys({
130
+ ...wallet,
131
+ node_url: this._settings.node_url,
132
+ start_height: this._settings.start_height,
133
+ });
134
+ }
135
+ /**
136
+ * get all wallets merged with env keys.
137
+ * wallets missing env keys are skipped.
138
+ */
139
+ async getWalletsOpened() {
140
+ const opened = [];
141
+ for (const wallet of this._settings.wallets) {
142
+ try {
143
+ const withKeys = await walletSettingsPlusKeys({
144
+ ...wallet,
145
+ node_url: this._settings.node_url,
146
+ start_height: this._settings.start_height,
147
+ });
148
+ opened.push(withKeys);
149
+ }
150
+ catch {
151
+ // wallet has no view key in env, skip
152
+ }
153
+ }
154
+ return opened;
155
+ }
156
+ walletExists(primary_address) {
157
+ return this._settings.wallets.some((w) => w.primary_address === primary_address);
158
+ }
159
+ /**
160
+ * add a new view wallet to settings and write its view key to env.
161
+ * if the wallet already exists, updates it instead.
162
+ */
163
+ async addViewWallet(primary_address, view_key, fields) {
164
+ primary_address = primary_address.trim();
165
+ view_key = view_key.trim();
166
+ ScanSettingsOpened._validateAddress(primary_address);
167
+ ScanSettingsOpened._validateViewKey(view_key);
168
+ ScanSettingsOpened._validateWalletName(fields?.wallet_name);
169
+ ScanSettingsOpened._validateWalletSlot(fields?.wallet_slot);
170
+ ScanSettingsOpened._validateWalletRoute(fields?.wallet_route);
171
+ ScanSettingsOpened._validateSubaddressIndex(fields?.subaddress_index);
172
+ ScanSettingsOpened._validateHalted(fields?.halted);
173
+ await this.reload();
174
+ const existing = this.getWallet(primary_address);
175
+ if (existing) {
176
+ throw new Error(`wallet already exists: ${primary_address} in ${this._scan_settings_path}`);
177
+ }
178
+ // write view key to env first
179
+ await writeViewKeyToDotEnv(primary_address, view_key);
180
+ const subaddress_index = fields?.subaddress_index ?? SUB_ADDRESS_INDEX_DEFAULT_VALUE;
181
+ this._settings.wallets.push({
182
+ primary_address,
183
+ ...fields,
184
+ subaddress_index,
185
+ });
186
+ await this._persist();
187
+ }
188
+ /**
189
+ * add a new view wallet to settings and write its view key to env.
190
+ * if the wallet already exists, updates it instead.
191
+ */
192
+ async addSpendWallet(wallet_secret, fields) {
193
+ // first part of writeWalletSecretsToDotEnv()
194
+ const spend_key = await makeSpendKeyFromSeed(wallet_secret.toHex());
195
+ const view_pair = await makeViewKey(spend_key);
196
+ const primary_address = view_pair.mainnet_primary;
197
+ ScanSettingsOpened._validateWalletName(fields?.wallet_name);
198
+ ScanSettingsOpened._validateWalletSlot(fields?.wallet_slot);
199
+ ScanSettingsOpened._validateWalletRoute(fields?.wallet_route);
200
+ ScanSettingsOpened._validateSubaddressIndex(fields?.subaddress_index);
201
+ ScanSettingsOpened._validateHalted(fields?.halted);
202
+ await this.reload();
203
+ const existing = this.getWallet(primary_address);
204
+ if (existing) {
205
+ throw new Error(`wallet already exists: ${primary_address} in ${this._scan_settings_path}`);
206
+ }
207
+ // second part of writeWalletSecretsToDotEnv(),
208
+ await writeEnvLineToDotEnvRefresh(`vk${primary_address}`, view_pair.view_key);
209
+ await writeEnvLineToDotEnvRefresh(`sk${primary_address}`, spend_key);
210
+ const subaddress_index = fields?.subaddress_index ?? SUB_ADDRESS_INDEX_DEFAULT_VALUE;
211
+ this._settings.wallets.push({
212
+ primary_address,
213
+ ...fields,
214
+ subaddress_index,
215
+ });
216
+ await this._persist();
217
+ }
218
+ /**
219
+ * remove a wallet from settings by primary address.
220
+ * does not remove the view key from env.
221
+ */
222
+ async removeWallet(primary_address) {
223
+ await this.reload();
224
+ this._settings.wallets = this._settings.wallets.filter((w) => w.primary_address !== primary_address);
225
+ await this._persist();
226
+ }
227
+ /**
228
+ * update specific fields on an existing wallet.
229
+ * set a field to null to unset it.
230
+ */
231
+ async updateWallet(primary_address, fields) {
232
+ await this.reload();
233
+ const wallet = this.getWallet(primary_address);
234
+ if (!wallet)
235
+ throw new Error(`wallet not found: ${primary_address} in ${this._scan_settings_path}`);
236
+ ScanSettingsOpened._validateWalletName(fields.wallet_name);
237
+ ScanSettingsOpened._validateWalletSlot(fields.wallet_slot);
238
+ ScanSettingsOpened._validateSubaddressIndex(fields.subaddress_index);
239
+ ScanSettingsOpened._validateWalletRoute(fields.wallet_route);
240
+ ScanSettingsOpened._validateHalted(fields.halted);
241
+ wallet.wallet_name = fields.wallet_name ?? wallet.wallet_name;
242
+ wallet.wallet_slot = fields.wallet_slot ?? wallet.wallet_slot;
243
+ wallet.wallet_route = fields.wallet_route ?? wallet.wallet_route;
244
+ wallet.subaddress_index =
245
+ fields.subaddress_index ?? wallet.subaddress_index;
246
+ wallet.halted = fields.halted ?? wallet.halted;
247
+ if (fields.halted === null)
248
+ delete wallet.halted;
249
+ if (fields.subaddress_index === null)
250
+ delete wallet.subaddress_index;
251
+ if (fields.wallet_route === null)
252
+ delete wallet.wallet_route;
253
+ if (fields.wallet_slot === null)
254
+ delete wallet.wallet_slot;
255
+ if (fields.wallet_name === null)
256
+ delete wallet.wallet_name;
257
+ await this._persist();
258
+ }
259
+ async haltWallet(primary_address) {
260
+ await this.updateWallet(primary_address, { halted: true });
261
+ }
262
+ async unhaltWallet(primary_address) {
263
+ await this.updateWallet(primary_address, { halted: false });
264
+ }
265
+ async setWalletName(primary_address, name) {
266
+ await this.updateWallet(primary_address, { wallet_name: name ?? null });
267
+ }
268
+ async setWalletSlot(primary_address, slot) {
269
+ await this.updateWallet(primary_address, { wallet_slot: slot ?? null });
270
+ }
271
+ async setWalletRoute(primary_address, route) {
272
+ await this.updateWallet(primary_address, { wallet_route: route ?? null });
273
+ }
274
+ async setSubaddressIndex(primary_address, index) {
275
+ await this.updateWallet(primary_address, {
276
+ subaddress_index: index ?? null,
277
+ });
278
+ }
279
+ /**
280
+ * reread the settings file from disk and replace inmemory state.
281
+ */
282
+ async reload() {
283
+ const settings = await openScanSettingsFile(this._scan_settings_path);
284
+ if (settings) {
285
+ ScanSettingsOpened._validateLogOptions(settings.logs_include, settings.logs_exclude);
286
+ this._settings = settings;
287
+ }
288
+ }
289
+ /**
290
+ * validate that all entries in logs_include and logs_exclude
291
+ * are valid function names from LOGGING_FUNCTIONS.
292
+ * throws if any entry is invalid.
293
+ */
294
+ static _validateLogOptions(logs_include, logs_exclude) {
295
+ const valid = new Set(LOGGING_FUNCTIONS);
296
+ const invalid = [];
297
+ for (const arr of [logs_include, logs_exclude]) {
298
+ if (!arr)
299
+ continue;
300
+ for (const item of arr) {
301
+ if (!valid.has(item)) {
302
+ invalid.push(item);
303
+ }
304
+ }
305
+ }
306
+ if (invalid.length > 0) {
307
+ throw new Error(`invalid log function(s): ${invalid.join(", ")}. ` +
308
+ `valid options are: ${[...valid].join(", ")}`);
309
+ }
310
+ }
311
+ static _validateAddress(address) {
312
+ if (!isValidMoneroAddress(address)) {
313
+ throw new Error(`invalid monero address: ${address}`);
314
+ }
315
+ }
316
+ static _validateViewKey(key) {
317
+ if (!isValidMoneroPrivateKey(key)) {
318
+ throw new Error(`invalid secret view key: ${key}`);
319
+ }
320
+ }
321
+ static _validateNodeUrl(url) {
322
+ let parsed;
323
+ try {
324
+ parsed = new URL(url);
325
+ }
326
+ catch {
327
+ throw new Error(`invalid node URL: ${url}`);
328
+ }
329
+ if (!["http:", "https:"].includes(parsed.protocol)) {
330
+ throw new Error(`unsupported protocol: ${parsed.protocol}`);
331
+ }
332
+ if (!parsed.hostname) {
333
+ throw new Error(`invalid node URL, missing hostname: ${url}`);
334
+ }
335
+ return url;
336
+ }
337
+ static _validateStartHeight(start_height) {
338
+ if (start_height !== null &&
339
+ (typeof start_height !== "number" || start_height < 0)) {
340
+ throw new Error(`invalid start height: ${start_height}. must be null or a number >= 0`);
341
+ }
342
+ }
343
+ static _validateCpuWorkerCount(cpu_worker_count) {
344
+ if (cpu_worker_count !== null &&
345
+ (typeof cpu_worker_count !== "number" || cpu_worker_count < 0)) {
346
+ throw new Error(`invalid cpu_worker_count: ${cpu_worker_count}. must be null or a number >= 0`);
347
+ }
348
+ }
349
+ static _validateMerchantConfirmations(merchant_confirmations) {
350
+ if (merchant_confirmations !== null &&
351
+ (typeof merchant_confirmations !== "number" || merchant_confirmations < 0)) {
352
+ throw new Error(`invalid merchant_confirmations: ${merchant_confirmations}. must be null or a number >= 0`);
353
+ }
354
+ }
355
+ static _validateWalletName(name) {
356
+ if (name === undefined || name === null)
357
+ return;
358
+ if (name.length > 100) {
359
+ throw new Error(`wallet name must be 100 characters or less, got ${name.length}`);
360
+ }
361
+ if (!/^[a-zA-Z0-9 _.-]+$/.test(name)) {
362
+ throw new Error(`wallet name must be alphanumeric with spaces, hyphens, underscores, or dots, got: ${name}`);
363
+ }
364
+ }
365
+ static _validateWalletSlot(slot) {
366
+ if (slot === undefined || slot === null)
367
+ return;
368
+ if (typeof slot !== "number" || slot < 0 || !Number.isInteger(slot)) {
369
+ throw new Error(`wallet slot must be a number >= 0, got: ${slot}`);
370
+ }
371
+ }
372
+ static _validateSubaddressIndex(index) {
373
+ if (index === undefined || index === null)
374
+ return;
375
+ if (typeof index !== "number" || index < 0 || !Number.isInteger(index)) {
376
+ throw new Error(`subaddress index must be a number >= 0, got: ${index}`);
377
+ }
378
+ }
379
+ static _validateWalletRoute(route) {
380
+ if (route === undefined || route === null)
381
+ return;
382
+ const result = walletRouteFromString(route);
383
+ if (!result.ok) {
384
+ throw new Error(result.error);
385
+ }
386
+ }
387
+ static _validateHalted(halted) {
388
+ if (halted === undefined || halted === null)
389
+ return;
390
+ if (typeof halted !== "boolean") {
391
+ throw new Error(`halted must be a boolean, got: ${halted}`);
392
+ }
393
+ }
394
+ _settings;
395
+ constructor(settings, _scan_settings_path, _pathPrefix) {
396
+ this._scan_settings_path = _scan_settings_path;
397
+ this._pathPrefix = _pathPrefix;
398
+ this._settings = settings;
399
+ }
400
+ /**
401
+ * write current in memory state to disk.
402
+ */
403
+ async _persist() {
404
+ await writeScanSettings(this._settings, this._scan_settings_path);
405
+ }
406
+ }
407
+ function isValidMoneroAddress(address) {
408
+ // basic sanity check
409
+ return (address.length >= 95 && address.length <= 106 && isAlphaNumeric(address));
410
+ }
411
+ function isValidMoneroPrivateKey(key) {
412
+ return key.length === 64 && isAlphaNumeric(key);
413
+ }
414
+ function isAlphaNumeric(str) {
415
+ return str.match(/^[a-z0-9]+$/i) !== null;
416
+ }
417
+ // strictly speaking according to the RFC 1035
418
+ // there is a limit of 63 chars per label
419
+ // and other rules like no leading or trailing hyphens, no dots in a row
420
+ function isValidDomainName(str) {
421
+ if (!str || str.length === 0 || str.length > 253) {
422
+ return false;
423
+ }
424
+ return str.match(/^[a-z0-9.\-_]+$/i) !== null;
425
+ }
426
+ // copied from seedphrase package to not introduce a dependency
427
+ export function walletRouteFromString(input) {
428
+ const parts = input.split("/");
429
+ if (parts.length < 1 || !parts[0]) {
430
+ return { ok: false, error: "missing identity" };
431
+ }
432
+ if (parts.length < 2 || !parts[1]) {
433
+ return { ok: false, error: "missing domain" };
434
+ }
435
+ if (parts.length < 3 || !parts[2]) {
436
+ return { ok: false, error: "missing wallet_type" };
437
+ }
438
+ if (parts.length < 4 || !parts[3]) {
439
+ return { ok: false, error: "missing wallet_slot" };
440
+ }
441
+ if (parts.length > 4) {
442
+ return {
443
+ ok: false,
444
+ error: "wallet route should only have 4 parts separated by /",
445
+ };
446
+ }
447
+ const [identity, domain, wallet_type, wallet_slot] = parts;
448
+ if (wallet_type !== "single" &&
449
+ wallet_type !== "sa_multi" &&
450
+ wallet_type !== "pl_multi") {
451
+ return { ok: false, error: `invalid wallet_type: "${wallet_type}"` };
452
+ }
453
+ if (Number.isNaN(parseInt(wallet_slot))) {
454
+ return { ok: false, error: `invalid wallet_slot: "${wallet_slot}"` };
455
+ }
456
+ if (parseInt(wallet_slot) < 0) {
457
+ return { ok: false, error: `invalid wallet_slot: "${wallet_slot} < 0"` };
458
+ }
459
+ if (!isAlphaNumeric(identity)) {
460
+ return {
461
+ ok: false,
462
+ error: `invalid identity, not alpha numeric: "${identity}"`,
463
+ };
464
+ }
465
+ if (!isValidDomainName(domain)) {
466
+ return {
467
+ ok: false,
468
+ error: `invalid domain, : "${domain}"`,
469
+ };
470
+ }
471
+ return {
472
+ ok: true,
473
+ route: { identity, domain, wallet_type, wallet_slot },
474
+ };
475
+ }
@@ -8,7 +8,7 @@ import { monero_wallet_api_wasm } from "../wasm-processing/wasmFile";
8
8
  import { WasmProcessor } from "../wasm-processing/wasmProcessor";
9
9
  import { LOCAL_NODE_DEFAULT_URL, } from "../node-interaction/nodeUrl";
10
10
  import { get_block_headers_range, get_info, } from "../api";
11
- import { readWalletFromScanSettings } from "../scanning-syncing/scanSettings";
11
+ import { readWalletFromScanSettings } from "../api";
12
12
  import { sleep } from "../io/sleep";
13
13
  /**
14
14
  * This class is useful to interact with Moneros DaemonRpc binary requests in a convenient way.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spirobel/monero-wallet-api",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "main": "dist/api.js",
5
5
  "module": "dist/api.js",
6
6
  "devDependencies": {