@spirobel/monero-wallet-api 0.5.2 → 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.
@@ -70,6 +70,7 @@ export declare class ScanCacheOpened {
70
70
  setMerchantConfirmations(merchant_confirmations: number | null): Promise<void>;
71
71
  setCpuWorkerCount(cpu_worker_count: number | undefined): Promise<void>;
72
72
  setLogSettings(logs?: LogSetting | null, logs_include?: PossibleLogs[] | null, logs_exclude?: PossibleLogs[] | null): Promise<void>;
73
+ setWalletName(name?: string): Promise<void>;
73
74
  retry(): Promise<boolean>;
74
75
  sendTransaction(signedTx: string): Promise<SendRawTransactionResult>;
75
76
  signTransaction(unsignedTx: string): Promise<string>;
@@ -200,6 +201,7 @@ export declare class ManyScanCachesOpened {
200
201
  setMerchantConfirmations(merchant_confirmations: number | null): Promise<void>;
201
202
  setCpuWorkerCount(cpu_worker_count: number | undefined): Promise<void>;
202
203
  setLogSettings(logs?: LogSetting | null, logs_include?: PossibleLogs[] | null, logs_exclude?: PossibleLogs[] | null): Promise<void>;
204
+ setWalletName(primary_address: string, name?: string): Promise<void>;
203
205
  changeStartHeight(start_height: number | null): Promise<void>;
204
206
  private static _buildWallets;
205
207
  static create(options: ManyScanCachesOpenedCreateOptions): Promise<ManyScanCachesOpened | undefined>;
@@ -208,6 +208,9 @@ export class ScanCacheOpened {
208
208
  await this._scanSettings.setLogSettings(logs, logs_include, logs_exclude);
209
209
  await this.unpause();
210
210
  }
211
+ async setWalletName(name) {
212
+ await this._scanSettings.setWalletName(this.view_pair.primary_address, name);
213
+ }
211
214
  async retry() {
212
215
  if (this.worker) {
213
216
  this.worker.terminate();
@@ -798,6 +801,9 @@ export class ManyScanCachesOpened {
798
801
  throw new Error("no wallets");
799
802
  return await this.wallets[0].setLogSettings(logs, logs_include, logs_exclude);
800
803
  }
804
+ async setWalletName(primary_address, name) {
805
+ await this._scanSettings.setWalletName(primary_address, name);
806
+ }
801
807
  async changeStartHeight(start_height) {
802
808
  if (this.wallets.length === 0)
803
809
  throw new Error("no wallets");
@@ -93,6 +93,17 @@ export declare class ScanSettingsOpened {
93
93
  * throws if any entry is invalid.
94
94
  */
95
95
  private static _validateLogOptions;
96
+ private static _validateAddress;
97
+ private static _validateViewKey;
98
+ private static _validateNodeUrl;
99
+ private static _validateStartHeight;
100
+ private static _validateCpuWorkerCount;
101
+ private static _validateMerchantConfirmations;
102
+ private static _validateWalletName;
103
+ private static _validateWalletSlot;
104
+ private static _validateSubaddressIndex;
105
+ private static _validateWalletRoute;
106
+ private static _validateHalted;
96
107
  private _settings;
97
108
  private constructor();
98
109
  /**
@@ -100,3 +111,17 @@ export declare class ScanSettingsOpened {
100
111
  */
101
112
  private _persist;
102
113
  }
114
+ export declare function walletRouteFromString(input: string): {
115
+ ok: boolean;
116
+ error: string;
117
+ route?: undefined;
118
+ } | {
119
+ ok: boolean;
120
+ route: {
121
+ identity: string;
122
+ domain: string;
123
+ wallet_type: string;
124
+ wallet_slot: string;
125
+ };
126
+ error?: undefined;
127
+ };
@@ -1,5 +1,5 @@
1
1
  import { LOCAL_NODE_DEFAULT_URL } from "../node-interaction/nodeUrl";
2
- import { LOGGING_FUNCTIONS } from "../io/logging";
2
+ import { LOGGING_FUNCTIONS, } from "../io/logging";
3
3
  import { getPathPrefix, makeSpendKeyFromSeed, makeViewKey, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, SUB_ADDRESS_INDEX_DEFAULT_VALUE, walletSettingsPlusKeys, writeEnvLineToDotEnvRefresh, writeScanSettings, writeViewKeyToDotEnv, } from "../api";
4
4
  export class ScanSettingsOpened {
5
5
  _scan_settings_path;
@@ -55,27 +55,34 @@ export class ScanSettingsOpened {
55
55
  }
56
56
  async setNodeUrl(node_url) {
57
57
  await this.reload();
58
- this._settings.node_url = node_url;
58
+ this._settings.node_url = ScanSettingsOpened._validateNodeUrl(node_url);
59
59
  await this._persist();
60
60
  }
61
61
  async setStartHeight(start_height) {
62
62
  await this.reload();
63
+ ScanSettingsOpened._validateStartHeight(start_height);
63
64
  this._settings.start_height = start_height;
64
65
  await this._persist();
65
66
  }
66
67
  async setMerchantConfirmations(merchant_confirmations) {
67
68
  await this.reload();
68
- this._settings.merchant_confirmations = merchant_confirmations;
69
- if (merchant_confirmations === null)
69
+ if (merchant_confirmations === undefined ||
70
+ merchant_confirmations === null) {
70
71
  delete this._settings.merchant_confirmations;
72
+ }
73
+ else {
74
+ ScanSettingsOpened._validateMerchantConfirmations(merchant_confirmations);
75
+ this._settings.merchant_confirmations = merchant_confirmations;
76
+ }
71
77
  await this._persist();
72
78
  }
73
79
  async setCpuWorkerCount(cpu_worker_count) {
74
80
  await this.reload();
75
- if (cpu_worker_count === null) {
81
+ if (cpu_worker_count === null || cpu_worker_count === undefined) {
76
82
  delete this._settings.cpu_worker_count;
77
83
  }
78
84
  else {
85
+ ScanSettingsOpened._validateCpuWorkerCount(cpu_worker_count);
79
86
  this._settings.cpu_worker_count = cpu_worker_count;
80
87
  }
81
88
  await this._persist();
@@ -154,13 +161,20 @@ export class ScanSettingsOpened {
154
161
  * if the wallet already exists, updates it instead.
155
162
  */
156
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);
157
173
  await this.reload();
158
174
  const existing = this.getWallet(primary_address);
159
175
  if (existing) {
160
176
  throw new Error(`wallet already exists: ${primary_address} in ${this._scan_settings_path}`);
161
177
  }
162
- primary_address = primary_address.trim();
163
- view_key = view_key.trim();
164
178
  // write view key to env first
165
179
  await writeViewKeyToDotEnv(primary_address, view_key);
166
180
  const subaddress_index = fields?.subaddress_index ?? SUB_ADDRESS_INDEX_DEFAULT_VALUE;
@@ -180,6 +194,11 @@ export class ScanSettingsOpened {
180
194
  const spend_key = await makeSpendKeyFromSeed(wallet_secret.toHex());
181
195
  const view_pair = await makeViewKey(spend_key);
182
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);
183
202
  await this.reload();
184
203
  const existing = this.getWallet(primary_address);
185
204
  if (existing) {
@@ -214,6 +233,11 @@ export class ScanSettingsOpened {
214
233
  const wallet = this.getWallet(primary_address);
215
234
  if (!wallet)
216
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);
217
241
  wallet.wallet_name = fields.wallet_name ?? wallet.wallet_name;
218
242
  wallet.wallet_slot = fields.wallet_slot ?? wallet.wallet_slot;
219
243
  wallet.wallet_route = fields.wallet_route ?? wallet.wallet_route;
@@ -284,6 +308,89 @@ export class ScanSettingsOpened {
284
308
  `valid options are: ${[...valid].join(", ")}`);
285
309
  }
286
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
+ }
287
394
  _settings;
288
395
  constructor(settings, _scan_settings_path, _pathPrefix) {
289
396
  this._scan_settings_path = _scan_settings_path;
@@ -297,3 +404,72 @@ export class ScanSettingsOpened {
297
404
  await writeScanSettings(this._settings, this._scan_settings_path);
298
405
  }
299
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spirobel/monero-wallet-api",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "main": "dist/api.js",
5
5
  "module": "dist/api.js",
6
6
  "devDependencies": {