@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
|
-
|
|
69
|
-
|
|
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
|
+
}
|