@spirobel/monero-wallet-api 0.4.0 → 0.4.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/dist/io/atomicWrite.js +2 -1
- package/dist/io/logging.d.ts +40 -0
- package/dist/io/logging.js +172 -0
- package/dist/scanning-syncing/backgroundWorker.d.ts +1 -0
- package/dist/scanning-syncing/backgroundWorker.js +19 -7
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +9 -7
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +2 -1
- package/dist/scanning-syncing/scanSettings.d.ts +10 -0
- package/dist/scanning-syncing/scanSettings.js +9 -0
- package/dist/scanning-syncing/scanresult/scanCache.js +7 -3
- package/dist/scanning-syncing/scanresult/scanCoordination.js +63 -72
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +1 -1
- package/dist/scanning-syncing/scanresult/scanResult.js +12 -3
- package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
- package/dist/scanning-syncing/worker-mains/coordinator-main.js +3 -2
- package/dist/scanning-syncing/worker-mains/cpubound-main.js +24 -14
- package/dist/scanning-syncing/worker-mains/worker.js +11 -7
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges,
|
|
1
|
+
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, } from "../../api";
|
|
2
2
|
import { markWorkItemAsDone, } from "./scanLoop";
|
|
3
3
|
import { makeWorkItem, scanLoop } from "./scanLoop";
|
|
4
4
|
import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../scanSettings";
|
|
5
5
|
import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
|
|
6
6
|
import { sendToCpuWorker } from "../worker-mains/cpubound-main";
|
|
7
|
+
import { log } from "../../io/logging";
|
|
7
8
|
/**
|
|
8
9
|
* this depends only on ScanSettings.json start_height and wallet caches scanned_ranges
|
|
9
10
|
* side effect: will init wallet cache file if it does not exist
|
|
@@ -59,7 +60,7 @@ export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_
|
|
|
59
60
|
for (const wallet_cache of wallet_caches) {
|
|
60
61
|
// only add the range to wallets that don't already have one
|
|
61
62
|
if (!findRange(wallet_cache.scanned_ranges, total_start_height)) {
|
|
62
|
-
|
|
63
|
+
log("findWorkToBeDone", "inserting range_at_start");
|
|
63
64
|
wallet_cache.scanned_ranges.push(range_at_start);
|
|
64
65
|
wallet_cache.scanned_ranges = mergeRanges(wallet_cache.scanned_ranges);
|
|
65
66
|
}
|
|
@@ -81,9 +82,9 @@ export async function findWorkToBeDone(scan_settings_path = SCAN_SETTINGS_STORE_
|
|
|
81
82
|
export function workToBeDoneForBatch(cache, batch_meta_infos) {
|
|
82
83
|
const begin_height = batch_meta_infos[0].block_height;
|
|
83
84
|
const end_height = batch_meta_infos[batch_meta_infos.length - 1].block_height;
|
|
84
|
-
|
|
85
|
+
log("workToBeDoneForBatch", [begin_height, end_height]);
|
|
85
86
|
const foundRange = findRange(cache.scanned_ranges, begin_height);
|
|
86
|
-
|
|
87
|
+
log("workToBeDoneForBatch", ["foundRange", foundRange]);
|
|
87
88
|
if (foundRange) {
|
|
88
89
|
const fullycovered = cache.scanned_ranges.find((r) => r.start <= begin_height && r.end > end_height);
|
|
89
90
|
if (fullycovered) {
|
|
@@ -96,13 +97,13 @@ export function workToBeDoneForBatch(cache, batch_meta_infos) {
|
|
|
96
97
|
throw new Error("[workToBeDoneForBatch] tip not found, malformed range that covers the work to be done for this batch");
|
|
97
98
|
const tipindex = findTipIndex(batch_meta_infos, tip);
|
|
98
99
|
if (tipindex === "reorg_found") {
|
|
99
|
-
|
|
100
|
+
log("workToBeDoneForBatch", "reorg found");
|
|
100
101
|
return { from: 0 };
|
|
101
102
|
}
|
|
102
103
|
else if (tipindex === "empty_blocks_array") {
|
|
103
104
|
return "skip";
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
log("workToBeDoneForBatch", ["tipindex", tipindex]);
|
|
106
107
|
return { from: tipindex }; // NORMAL case of scheduling directly in front of the tip
|
|
107
108
|
}
|
|
108
109
|
}
|
|
@@ -123,36 +124,14 @@ export function makeWorkItemsFromBlocksBuffer(blocksBuffer, workItemBuffer, wall
|
|
|
123
124
|
const alreadyReferenced = workItemBuffer.some((w) => w.batch.local_uuid === batch.local_uuid &&
|
|
124
125
|
w.walletConfig.primary_address === walletConfig.primary_address);
|
|
125
126
|
if (!alreadyReferenced) {
|
|
126
|
-
// const begin_height =
|
|
127
|
-
// batch.get_blocks_result_meta.block_infos[0].block_height;
|
|
128
|
-
// const end_height =
|
|
129
|
-
// batch.get_blocks_result_meta.block_infos[
|
|
130
|
-
// batch.get_blocks_result_meta.block_infos.length - 1
|
|
131
|
-
// ].block_height;
|
|
132
|
-
// console.log(walletConfig.cache.scanned_ranges);
|
|
133
|
-
// console.log(begin_height);
|
|
134
|
-
// // skip batches fully covered by an existing scanned range
|
|
135
|
-
// if (
|
|
136
|
-
// walletConfig.cache.scanned_ranges.some(
|
|
137
|
-
// (r) => r.start <= begin_height && r.end >= end_height,
|
|
138
|
-
// )
|
|
139
|
-
// ) {
|
|
140
|
-
// throw new Error(
|
|
141
|
-
// "skip" +
|
|
142
|
-
// begin_height +
|
|
143
|
-
// " end_height" +
|
|
144
|
-
// end_height +
|
|
145
|
-
// " " +
|
|
146
|
-
// walletConfig.primary_address.slice(0, 6),
|
|
147
|
-
// );
|
|
148
|
-
// continue;
|
|
149
|
-
// }
|
|
150
127
|
const workToBeDone = workToBeDoneForBatch(walletConfig.cache, batch.get_blocks_result_meta.block_infos);
|
|
151
128
|
if (workToBeDone === "skip") {
|
|
152
129
|
continue;
|
|
153
130
|
}
|
|
154
131
|
const workItem = makeWorkItem(walletConfig, batch, from, to);
|
|
155
|
-
|
|
132
|
+
log("makeWorkItemsFromBlocksBuffer", [
|
|
133
|
+
`uuid=${workItem.work_uuid.slice()} to=${workItem.to} from=${workItem.from} batchbegin_height=${batch.get_blocks_result_meta.block_infos[0].block_height} batchend_height=${batch.get_blocks_result_meta.block_infos[batch.get_blocks_result_meta.block_infos.length - 1].block_height}`,
|
|
134
|
+
]);
|
|
156
135
|
workItemBuffer.push(workItem);
|
|
157
136
|
}
|
|
158
137
|
}
|
|
@@ -168,16 +147,16 @@ export function makeWorkItemsForAllWallets(wallet_configs, blocksBuffer, workBuf
|
|
|
168
147
|
* blocks buffer if no remaining work items reference it.
|
|
169
148
|
*/
|
|
170
149
|
export function reconcileWorkItemDone(blocksBuffer, workItemBuffer) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
150
|
+
log("reconcileWorkItemDone", [
|
|
151
|
+
`workItemBuffer.length=${workItemBuffer.length} , blocksBuffer.length=${blocksBuffer.length}`,
|
|
152
|
+
]);
|
|
174
153
|
while (workItemBuffer.length > 0 &&
|
|
175
154
|
workItemBuffer[0].status === "process_result_done") {
|
|
176
155
|
const removed = workItemBuffer.shift();
|
|
177
156
|
const stillReferenced = workItemBuffer.some((w) => w.batch.local_uuid === removed.batch.local_uuid);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
157
|
+
log("reconcileWorkItemDone", [
|
|
158
|
+
`workItem: ${removed.work_uuid.slice()} removed. stillReferenced=${stillReferenced}`,
|
|
159
|
+
]);
|
|
181
160
|
if (!stillReferenced) {
|
|
182
161
|
const idx = blocksBuffer.findIndex((b) => b.local_uuid === removed.batch.local_uuid);
|
|
183
162
|
// this really means we have to save work items scanCache to file
|
|
@@ -218,17 +197,20 @@ export async function processScanResultForWorkItem(value, workBuffer, blocksBuff
|
|
|
218
197
|
if (value.type !== "Ready" || !value.result || !value.work_uuid)
|
|
219
198
|
return;
|
|
220
199
|
const item = await markWorkItemAsDone(value, workBuffer);
|
|
221
|
-
|
|
200
|
+
log("processScanResultForWorkItem", `item_work_uuid=${item?.work_uuid}`);
|
|
222
201
|
if (!item || item.status !== "scanwork_done")
|
|
223
202
|
throw new Error("[processScanResultForWorkItem] item not found or not scanwork not done. item_status=" +
|
|
224
203
|
item?.status);
|
|
225
204
|
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
log("processScanResultForWorkItem", [
|
|
206
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} firstBlock=${firstBlock?.block_height} firstBlockHash=${firstBlock?.block_hash?.slice(0, 8)}`,
|
|
207
|
+
]);
|
|
208
|
+
log("processScanResultForWorkItem", [
|
|
209
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
|
|
210
|
+
]);
|
|
229
211
|
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
230
212
|
const cache = item.walletConfig.cache;
|
|
231
|
-
await
|
|
213
|
+
await processScanResult({
|
|
232
214
|
from_height: firstBlock.block_height,
|
|
233
215
|
to_height: lastBlock.block_height,
|
|
234
216
|
result: value.result,
|
|
@@ -263,28 +245,27 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
|
|
|
263
245
|
}
|
|
264
246
|
}
|
|
265
247
|
const firstBlock = item.batch.get_blocks_result_meta.block_infos[item.from];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
248
|
+
log("processWorkItem", [
|
|
249
|
+
`block_infos.length=${item.batch.get_blocks_result_meta.block_infos.length} from=${item.from} to=${item.to}`,
|
|
250
|
+
]);
|
|
269
251
|
const lastBlock = item.batch.get_blocks_result_meta.block_infos[item.to];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// );
|
|
252
|
+
log("processWorkItem", [
|
|
253
|
+
item.walletConfig.primary_address.slice(0, 6),
|
|
254
|
+
"@",
|
|
255
|
+
firstBlock.block_height,
|
|
256
|
+
"-",
|
|
257
|
+
lastBlock.block_height,
|
|
258
|
+
]);
|
|
278
259
|
let res;
|
|
279
260
|
try {
|
|
280
|
-
res = await
|
|
261
|
+
res = await processScanResult({
|
|
281
262
|
from_height: firstBlock.block_height,
|
|
282
263
|
to_height: lastBlock.block_height,
|
|
283
264
|
result: item.result,
|
|
284
265
|
scanCache: cache,
|
|
285
266
|
secret_spend_key,
|
|
286
267
|
});
|
|
287
|
-
|
|
268
|
+
log("processWorkItem", ["res=", res]);
|
|
288
269
|
}
|
|
289
270
|
catch (error) {
|
|
290
271
|
console.error("[processWorkItem] error=", error);
|
|
@@ -292,7 +273,7 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
|
|
|
292
273
|
}
|
|
293
274
|
await writeCacheToFile(cache, pathPrefix);
|
|
294
275
|
item.status = "process_result_done";
|
|
295
|
-
|
|
276
|
+
log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
|
|
296
277
|
// remove from blocksbuffer if no more work items reference it
|
|
297
278
|
reconcileWorkItemDone(blocksBuffer, workBuffer);
|
|
298
279
|
//because cache is tied to the workitems by reference,
|
|
@@ -314,7 +295,9 @@ function logBufStatus(blocksBuffer, workBuffer, ports, label) {
|
|
|
314
295
|
w.batch.get_blocks_result_meta.block_infos[w.from]?.block_height +
|
|
315
296
|
"-" +
|
|
316
297
|
w.batch.get_blocks_result_meta.block_infos[w.to]?.block_height);
|
|
317
|
-
|
|
298
|
+
log("logBufStatus", [
|
|
299
|
+
`[buf] ${label} blocks=[${bb.join(",")}] work=[${wb.join(",")}]`,
|
|
300
|
+
]);
|
|
318
301
|
}
|
|
319
302
|
/**
|
|
320
303
|
* coordinator main: async generator that drives the full scan cycle.
|
|
@@ -353,10 +336,9 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
|
|
|
353
336
|
races.push(p.then((v) => ({ src: "scan", addr, value: v })));
|
|
354
337
|
}
|
|
355
338
|
const winner = await Promise.race(races);
|
|
356
|
-
|
|
357
|
-
winner.src +
|
|
358
|
-
|
|
359
|
-
String(winner.addr ?? "").slice(0, 8));
|
|
339
|
+
log("coordinatorMain", [
|
|
340
|
+
"winner=" + winner.src + " addr=" + String(winner.addr ?? "").slice(0, 8),
|
|
341
|
+
]);
|
|
360
342
|
if (winner.src === "blocks") {
|
|
361
343
|
const result = winner.value.value;
|
|
362
344
|
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
@@ -368,7 +350,7 @@ export async function* coordinatorMain(scanSettingsPath, pathPrefix) {
|
|
|
368
350
|
if (scanPromises.has(addr))
|
|
369
351
|
continue;
|
|
370
352
|
const gen = scanGens.get(addr);
|
|
371
|
-
|
|
353
|
+
log("coordinatorMain", ["scanGens.get(addr)", gen]);
|
|
372
354
|
if (!gen)
|
|
373
355
|
continue;
|
|
374
356
|
const item = workBuffer.find((x) => x.status === "fresh" && x.walletConfig.primary_address === addr);
|
|
@@ -459,7 +441,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
459
441
|
// throw new Error(
|
|
460
442
|
// "[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker",
|
|
461
443
|
// );
|
|
462
|
-
|
|
444
|
+
log("coordinatorMainMultithreaded", "fallback to single-threaded");
|
|
463
445
|
yield* coordinatorMain(scanSettingsPath, pathPrefix);
|
|
464
446
|
return;
|
|
465
447
|
}
|
|
@@ -482,7 +464,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
482
464
|
let race_count = 0;
|
|
483
465
|
while (true) {
|
|
484
466
|
race_count++;
|
|
485
|
-
|
|
467
|
+
log("coordinatorMainMultithreaded", ["race_count", race_count]);
|
|
486
468
|
await scheduleWorkOnCpuPorts(freePorts, workBuffer);
|
|
487
469
|
const scan_promises = freePorts
|
|
488
470
|
.filter((ps) => ps.promise)
|
|
@@ -494,9 +476,9 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
494
476
|
value: v,
|
|
495
477
|
})),
|
|
496
478
|
];
|
|
497
|
-
|
|
479
|
+
log("coordinatorMainMultithreaded", ["races", races]);
|
|
498
480
|
const winner = await Promise.race(races);
|
|
499
|
-
|
|
481
|
+
log("coordinatorMainMultithreaded", "cpu ports status");
|
|
500
482
|
if ("src" in winner && winner.src === "blocks") {
|
|
501
483
|
const result = winner.value.value;
|
|
502
484
|
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
@@ -529,7 +511,7 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
529
511
|
if (!to_be_processed)
|
|
530
512
|
continue;
|
|
531
513
|
const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
|
|
532
|
-
//
|
|
514
|
+
// log("coordinatorMainMultithreaded", "processWorkItem result");
|
|
533
515
|
yield {
|
|
534
516
|
type: "scan_ready",
|
|
535
517
|
address: wallet.primary_address,
|
|
@@ -561,9 +543,14 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
561
543
|
const onmessage = (event) => {
|
|
562
544
|
const msg = event.data;
|
|
563
545
|
// handle the result here:
|
|
564
|
-
|
|
546
|
+
log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
|
|
565
547
|
if (msg.work_uuid !== item.work_uuid) {
|
|
566
|
-
|
|
548
|
+
log("scheduleWorkOnCpuPorts", [
|
|
549
|
+
"wrong work_uuid in msg msg.work_uuid=",
|
|
550
|
+
msg.work_uuid,
|
|
551
|
+
"item.work_uuid=",
|
|
552
|
+
item.work_uuid,
|
|
553
|
+
]);
|
|
567
554
|
throw new Error("[scheduleWorkOnCpuPorts] wrong work_uuid in msg");
|
|
568
555
|
}
|
|
569
556
|
if (msg.type === "WORKSTART") {
|
|
@@ -571,7 +558,7 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
571
558
|
return;
|
|
572
559
|
}
|
|
573
560
|
if (item.status !== "scanwork_in_progress") {
|
|
574
|
-
|
|
561
|
+
log("scheduleWorkOnCpuPorts", "wrong status in msg");
|
|
575
562
|
throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
|
|
576
563
|
}
|
|
577
564
|
item.result = msg.result;
|
|
@@ -581,7 +568,7 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
581
568
|
};
|
|
582
569
|
port_status.port.onmessage = onmessage;
|
|
583
570
|
item.status = "scanwork_in_progress";
|
|
584
|
-
|
|
571
|
+
log("scheduleWorkOnCpuPorts", ["scheduling work item", item.work_uuid]);
|
|
585
572
|
port_status.promise = new Promise((resolve) => {
|
|
586
573
|
resolve_port = resolve;
|
|
587
574
|
});
|
|
@@ -604,7 +591,11 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
604
591
|
break;
|
|
605
592
|
}
|
|
606
593
|
catch {
|
|
607
|
-
|
|
594
|
+
log("scheduleWorkOnCpuPorts", [
|
|
595
|
+
"resend attempt",
|
|
596
|
+
attempt,
|
|
597
|
+
item.work_uuid,
|
|
598
|
+
]);
|
|
608
599
|
}
|
|
609
600
|
}
|
|
610
601
|
}
|
|
@@ -22,7 +22,7 @@ export type ProcessScanResult = {
|
|
|
22
22
|
current_range: CacheRange;
|
|
23
23
|
changed_outputs: ChangedOutput[];
|
|
24
24
|
};
|
|
25
|
-
export declare function
|
|
25
|
+
export declare function processScanResult(params: ProcessScanResultParamsWithoutSideEffects): Promise<ProcessScanResult>;
|
|
26
26
|
export declare function processResultReturnValue(cache: ScanCache, params: ProcessScanResultParamsWithoutSideEffects, changed_outputs: ChangedOutput[]): {
|
|
27
27
|
current_range: CacheRange;
|
|
28
28
|
changed_outputs: ChangedOutput[];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CatastrophicReorgError, } from "../../api";
|
|
2
2
|
import { computeKeyImage } from "./computeKeyImage";
|
|
3
|
+
import { log } from "../../io/logging";
|
|
3
4
|
import { mergeRanges, findRange, readCacheFileDefaultLocation, lastRange, writeCacheToFile, findRangeThrows, } from "./scanCache";
|
|
4
5
|
import {} from "../../node-interaction/binaryEndpoints";
|
|
5
|
-
export async function
|
|
6
|
+
export async function processScanResult(params) {
|
|
6
7
|
const { result, scanCache: cache, secret_spend_key } = params;
|
|
7
8
|
let changed_outputs = [];
|
|
8
9
|
if (!(result && "primary_address" in result))
|
|
@@ -25,7 +26,10 @@ export async function processScanResultWITHOUT_SIDE_EFFECTS(params) {
|
|
|
25
26
|
throw new CatastrophicReorgError("could not locate anchors, should not try to processResult on stale workItems");
|
|
26
27
|
const split_height = result.block_infos[split_height_index];
|
|
27
28
|
// we found the split height & do the reorg
|
|
28
|
-
|
|
29
|
+
log("processScanResult", [
|
|
30
|
+
"SPLIT FOUND at height",
|
|
31
|
+
split_height.block_height,
|
|
32
|
+
]);
|
|
29
33
|
if (!cache.reorg_info) {
|
|
30
34
|
cache.reorg_info = {
|
|
31
35
|
split_heights: [split_height],
|
|
@@ -41,7 +45,12 @@ export async function processScanResultWITHOUT_SIDE_EFFECTS(params) {
|
|
|
41
45
|
const reverted_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.spent_block_height !== undefined &&
|
|
42
46
|
output.spent_block_height >= split_height.block_height);
|
|
43
47
|
const removed_outputs = Object.entries(cache.outputs).filter(([id, output]) => output.block_height >= split_height.block_height);
|
|
44
|
-
|
|
48
|
+
log("processScanResult", [
|
|
49
|
+
"removed_outputs count:",
|
|
50
|
+
removed_outputs.length,
|
|
51
|
+
"reverted count:",
|
|
52
|
+
reverted_outputs.length,
|
|
53
|
+
]);
|
|
45
54
|
for (const [id, old_output_state] of removed_outputs) {
|
|
46
55
|
// 1. find key_image of output to be removed (as it was reorged)
|
|
47
56
|
const [key_image] = Object.entries(cache.own_key_images).find(([own_key_image, globalid]) => globalid === id) || [""]; // if this is viewonly the key_image will be empty
|