@zktx.io/sui-mcp 0.7.0 → 0.8.0
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 +83 -42
- package/dist/core/activity/localDataService.js +8 -21
- package/dist/core/activity/localDataValidation.js +22 -46
- package/dist/core/activity/sqliteAccountStore.js +46 -0
- package/dist/core/activity/sqliteActivityStore.js +65 -147
- package/dist/core/activity/sqliteActivityStoreSchema.js +42 -26
- package/dist/core/activity/transactionActivityService.js +2 -2
- package/dist/core/preferences/preferencesStore.js +2 -2
- package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
- package/dist/core/session/cardResult.js +41 -0
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +51 -19
- package/dist/core/session/chainAccounts.js +120 -0
- package/dist/core/session/sessionStore.js +4 -8
- package/dist/core/session/settingsFileStore.js +3 -30
- package/dist/core/session/settingsOperation.js +27 -19
- package/dist/core/session/settingsWorkflow.js +161 -87
- package/dist/core/session/sqliteCardStore.js +34 -14
- package/dist/core/session/sqliteSessionStore.js +5 -5
- package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
- package/dist/core/session/sqliteTransactionRequest.js +18 -0
- package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/wait.js +6 -11
- package/dist/core/session/walletConnection.js +71 -46
- package/dist/core/session/walletConnectionOperations.js +24 -0
- package/dist/core/session/walletRuntime.js +7 -13
- package/dist/core/session/walletWorkflow.js +248 -99
- package/dist/core/session/workflowState.js +104 -38
- package/dist/core/session/workflowView.js +92 -101
- package/dist/mcp/activeAccountResponse.js +3 -0
- package/dist/mcp/responseGuidance.js +7 -9
- package/dist/mcp/serverInfo.js +11 -10
- package/dist/mcp/toolErrors.js +5 -3
- package/dist/mcp/toolNames.js +1 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
- package/dist/mcp/tools/read/readToolHelpers.js +9 -7
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +2 -2
- package/dist/mcp/tools/session/statusTools.js +12 -10
- package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
- package/dist/mcp/tools/settings/index.js +7 -4
- package/dist/mcp-app/account.html +7 -7
- package/dist/mcp-app/chart.html +3 -3
- package/dist/mcp-app/connect.html +13 -13
- package/dist/mcp-app/disconnect.html +3637 -0
- package/dist/mcp-app/disconnect.notices.txt +1480 -0
- package/dist/mcp-app/receipt.html +393 -393
- package/dist/mcp-app/review.html +459 -459
- package/dist/mcp-app/settings.html +7 -7
- package/dist/mcp-ui/contracts.js +3 -1
- package/dist/mcp-ui/tools.js +4 -4
- package/dist/runtime/application.js +1 -1
- package/dist/runtime/localSettingsService.js +7 -4
- package/dist/runtime/smokeMainnetRead.js +5 -4
- package/dist/runtime/walletConnectTransport.js +16 -16
- package/dist/runtime/walletSdkChildRuntime.js +29 -12
- package/dist/runtime/walletSdkIpc.js +5 -4
- package/dist/runtime/walletSdkProcess.js +24 -8
- package/docs/AGENT_BEHAVIOR.md +82 -54
- package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
- package/docs/MCP_SETUP.md +319 -339
- package/docs/MCP_TOOLS.md +292 -177
- package/docs/SDK_API.md +15 -6
- package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
- package/docs/UTILITY_INDEX.md +9 -7
- package/docs/WALLET_CONNECTION.md +162 -167
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
|
-
import { TextDecoder } from "node:util";
|
|
3
|
+
import { TextDecoder, isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { LocalDataError } from "../activity/localDataTypes.js";
|
|
5
5
|
import { settingsInputError, SETTINGS_REPLACEMENT_BLOCKED_MESSAGE } from "./settingsInputError.js";
|
|
6
6
|
import { SETTINGS_APPLIES_AFTER_RESTART } from "../preferences/preferencesStore.js";
|
|
@@ -10,7 +10,7 @@ import { DEFAULT_SESSION_TTL_MS } from "./localSession.js";
|
|
|
10
10
|
import { isSettingsDownloadFile, settingsDownloadPath } from "./settingsFileIdentity.js";
|
|
11
11
|
import { SettingsDownloadPathExists, settingsFileDigest, decodeSettingsChunk } from "./settingsFileStore.js";
|
|
12
12
|
import { settingsIdentity } from "./sqliteSettingsWorkflowStore.js";
|
|
13
|
-
import { SettingsConflict, settingsDownload, isSettingsDownloadFileAction, settingsViewSchema, endpointPreparationSchema, storedRestoreCheckSchema, retainedBackupRejection, permitsBackupRecheck } from "./settingsOperation.js";
|
|
13
|
+
import { SettingsConflict, settingsCardInputSchema, settingsDownload, isSettingsDownloadFileAction, settingsViewSchema, endpointPreparationSchema, storedRestoreCheckSchema, retainedBackupRejection, permitsBackupRecheck } from "./settingsOperation.js";
|
|
14
14
|
export const DOWNLOAD_RETRY_WINDOW_MS = DEFAULT_SESSION_TTL_MS;
|
|
15
15
|
export class SettingsWorkflow {
|
|
16
16
|
options;
|
|
@@ -29,7 +29,29 @@ export class SettingsWorkflow {
|
|
|
29
29
|
runtimeEvent: (event) => this.runtimeEvent(event), readyPublished: (runId) => this.readyPublished(runId) });
|
|
30
30
|
}
|
|
31
31
|
get records() { this.options.assertCurrent(); return this.options.records; }
|
|
32
|
-
create(record) {
|
|
32
|
+
async create(record) {
|
|
33
|
+
const input = settingsCardInputSchema.parse(record.state.input);
|
|
34
|
+
if (!input.operationStatus) {
|
|
35
|
+
this.records.create(record);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const operation = this.records.get(input.operationStatus.operationId);
|
|
39
|
+
if (!operation || operation.dataEpoch !== this.records.control().data_epoch) {
|
|
40
|
+
throw new SettingsConflict("This Settings request is not available in the current profile and server run.");
|
|
41
|
+
}
|
|
42
|
+
const original = this.records.cards.get(operation.cardId);
|
|
43
|
+
if (!original)
|
|
44
|
+
throw new SettingsConflict("This Settings request is unavailable.");
|
|
45
|
+
const download = settingsDownload(operation);
|
|
46
|
+
const targetId = download?.localSave?.attempts.find((a) => a.saveAttemptId === download.localSave?.currentSaveAttemptId)?.cardId ??
|
|
47
|
+
download?.attempts.find((a) => a.attemptId === download.currentAttemptId)?.cardId;
|
|
48
|
+
const target = targetId ? this.records.cards.get(targetId) ?? original : original;
|
|
49
|
+
const current = await this.describe(target, true);
|
|
50
|
+
const { restoreCheck: _restoreCheck, ...status } = current.data;
|
|
51
|
+
const data = { ...status, allowedActions: [], observe: false,
|
|
52
|
+
...(current.data.download ? { download: { ...current.data.download, allowedFileActions: [] } } : {}) };
|
|
53
|
+
this.records.cards.create({ ...record, state: { ...record.state, state: "closed", reason: "completed", data } });
|
|
54
|
+
}
|
|
33
55
|
operationKind(record) { return this.records.forCard(record).draft?.kind; }
|
|
34
56
|
recoverOwner() {
|
|
35
57
|
this.records.transaction(() => {
|
|
@@ -51,25 +73,37 @@ export class SettingsWorkflow {
|
|
|
51
73
|
const card = this.records.cards.get(operation.cardId);
|
|
52
74
|
if (!card)
|
|
53
75
|
continue;
|
|
54
|
-
const verifyExport = operation.draft?.kind === "export_local_data" && file?.role === "export" && !!operation.admittedAt;
|
|
55
76
|
const result = { outcome: "interrupted", recordedAt: this.now().toISOString(),
|
|
56
77
|
message: operation.admittedAt
|
|
57
78
|
? "Sui MCP restarted before this confirmed change finished. It will not be repeated. Check the result of the original request separately."
|
|
58
79
|
: "Sui MCP restarted before you confirmed this change. It will not be repeated." };
|
|
59
|
-
this.records.db.prepare(`UPDATE settings_operations SET owner_id=?,phase
|
|
60
|
-
.run(this.records.ownerId,
|
|
80
|
+
this.records.db.prepare(`UPDATE settings_operations SET owner_id=?,phase='terminal',result_json=? WHERE operation_id=?`)
|
|
81
|
+
.run(this.records.ownerId, JSON.stringify(result), operation.operationId);
|
|
61
82
|
const reason = card.state.reason ?? (card.state.state === "ready" && Date.parse(card.state.expiresAt) <= this.now().getTime() ? "expired" : "server_restarted");
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
this.records.invalidateUpload(operation);
|
|
84
|
+
if (card.state.state !== "closed" && !this.records.cards.replace(card, { ...card, ownerId: this.records.ownerId,
|
|
85
|
+
state: { ...card.state, state: "closed", reason, revision: card.state.revision + 1,
|
|
86
|
+
data: this.records.resultView(operation, result) } }, this.now())) {
|
|
64
87
|
throw new SettingsConflict("The interrupted Settings card changed while its status was being updated.");
|
|
65
88
|
}
|
|
66
|
-
if (file?.role === "upload")
|
|
67
|
-
this.records.writeFile({ ...file, invalidated: true });
|
|
68
89
|
this.records.db.prepare(`UPDATE settings_control SET active_operation_id=NULL,wallet_input_blocked=0,wallet_block_operation_id=NULL
|
|
69
90
|
WHERE id=1 AND active_operation_id=?`).run(operation.operationId);
|
|
70
91
|
if (file)
|
|
71
92
|
this.records.releaseFileIO(file.fileId);
|
|
72
93
|
}
|
|
94
|
+
const pendingCards = this.records.db.prepare("SELECT id FROM live_read_cards WHERE kind='settings' AND state!='closed' AND owner_id!=?")
|
|
95
|
+
.all(this.records.ownerId);
|
|
96
|
+
for (const { id } of pendingCards) {
|
|
97
|
+
const card = this.records.cards.get(id), operation = this.records.get(card.operationId);
|
|
98
|
+
if (!operation)
|
|
99
|
+
throw new SettingsConflict("The interrupted Settings operation is unavailable.");
|
|
100
|
+
const result = { outcome: "interrupted", recordedAt: this.now().toISOString(),
|
|
101
|
+
message: "This card ended when the server restarted. Check the original operation's current result in a new status card; no effect will be repeated." };
|
|
102
|
+
if (!this.records.cards.replace(card, { ...card, ownerId: this.records.ownerId,
|
|
103
|
+
state: { ...card.state, state: "closed", reason: "server_restarted", revision: card.state.revision + 1,
|
|
104
|
+
data: this.records.resultView(operation, result) } }, this.now()))
|
|
105
|
+
throw new SettingsConflict("The interrupted Settings card changed during recovery.");
|
|
106
|
+
}
|
|
73
107
|
const ioId = this.records.control().file_io_id;
|
|
74
108
|
if (ioId) {
|
|
75
109
|
const file = this.records.file(ioId);
|
|
@@ -159,23 +193,8 @@ export class SettingsWorkflow {
|
|
|
159
193
|
await this.cleanupTemporaryFiles();
|
|
160
194
|
});
|
|
161
195
|
}
|
|
162
|
-
async verifyPriorExport(operation) {
|
|
163
|
-
const file = operation.fileId ? this.records.file(operation.fileId) : undefined;
|
|
164
|
-
let result = { outcome: "interrupted", recordedAt: this.now().toISOString(),
|
|
165
|
-
message: "Sui MCP restarted and the backup file could not be checked. Open a new Settings card to create another backup." };
|
|
166
|
-
if (file) {
|
|
167
|
-
try {
|
|
168
|
-
await this.options.files.verifyExport(file);
|
|
169
|
-
result = { outcome: "completed", recordedAt: this.now().toISOString(), message: "The previously created local backup was verified.",
|
|
170
|
-
details: { path: this.options.files.exportPath(file), filename: file.displayName, fileId: file.fileId, sha256: file.sha256, totalSize: file.totalSize } };
|
|
171
|
-
}
|
|
172
|
-
catch { }
|
|
173
|
-
}
|
|
174
|
-
this.options.assertCurrent();
|
|
175
|
-
this.retainResult(operation.operationId, result, false, file && result.outcome === "completed" ? { ...file, sealed: true, receivedSize: file.totalSize } : undefined);
|
|
176
|
-
}
|
|
177
196
|
async readFile(record, fileId, offset, limit, attemptId) {
|
|
178
|
-
const operation = this.records.forCard(record)
|
|
197
|
+
const operation = this.records.forCard(record);
|
|
179
198
|
if (operation.draft?.kind === "download_backup") {
|
|
180
199
|
if (this.closing)
|
|
181
200
|
throw new SettingsConflict("This server is closing; the download cannot continue.");
|
|
@@ -204,13 +223,7 @@ export class SettingsWorkflow {
|
|
|
204
223
|
await this.cleanupTemporaryFiles(operation.operationId);
|
|
205
224
|
}
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
!file || file.operationId !== operation.operationId || !file.sealed || file.invalidated || attemptId !== undefined)
|
|
209
|
-
throw new SettingsConflict("This completed backup is not available for download.");
|
|
210
|
-
const length = Math.min(limit, file.totalSize - offset);
|
|
211
|
-
const data = await this.options.files.readExportRange(file, offset, length);
|
|
212
|
-
this.options.assertCurrent();
|
|
213
|
-
return { file, offset, chunkBase64: data.toString("base64") };
|
|
226
|
+
throw new SettingsConflict("This Settings request does not provide a download.");
|
|
214
227
|
}
|
|
215
228
|
async startDownload(expected, input) {
|
|
216
229
|
let bytes;
|
|
@@ -231,7 +244,7 @@ export class SettingsWorkflow {
|
|
|
231
244
|
await this.trackDownloadIO(file.fileId, () => this.options.files.createDownload(file, data));
|
|
232
245
|
this.options.assertCurrent();
|
|
233
246
|
readyAt = this.now().toISOString();
|
|
234
|
-
result = { outcome: "completed", completionScope: "
|
|
247
|
+
result = { outcome: "completed", completionScope: "backup_delivery", recordedAt: readyAt,
|
|
235
248
|
message: "The backup is ready for download. This does not confirm that a file was saved.",
|
|
236
249
|
details: { filename: file.displayName, fileId: file.fileId, sha256: file.sha256, totalSize: file.totalSize } };
|
|
237
250
|
}
|
|
@@ -243,7 +256,7 @@ export class SettingsWorkflow {
|
|
|
243
256
|
const existing = this.records.file(file.fileId);
|
|
244
257
|
this.records.writeFile({ ...existing, invalidated: true, cleanup: "complete" });
|
|
245
258
|
});
|
|
246
|
-
result = { outcome: "failed", completionScope: "
|
|
259
|
+
result = { outcome: "failed", completionScope: "backup_delivery", recordedAt: this.now().toISOString(),
|
|
247
260
|
message: "The backup could not be prepared. Open a new Settings card to try again." };
|
|
248
261
|
}
|
|
249
262
|
this.finishedFileIO.add(file.fileId);
|
|
@@ -304,7 +317,8 @@ export class SettingsWorkflow {
|
|
|
304
317
|
retained = { saveAttemptId, outcome };
|
|
305
318
|
this.localSaveResults.set(file.operationId, retained);
|
|
306
319
|
}
|
|
307
|
-
this.
|
|
320
|
+
const path = retained.outcome.status === "saved" ? this.options.files.localBackupPath(file) : undefined;
|
|
321
|
+
this.records.settleLocalSave(file.operationId, retained.saveAttemptId, retained.outcome, path ? { directory: dirname(path), filename: basename(path) } : undefined);
|
|
308
322
|
this.localSaveResults.delete(file.operationId);
|
|
309
323
|
this.scheduleDownloadExpiry();
|
|
310
324
|
await this.cleanupTemporaryFiles(file.operationId);
|
|
@@ -352,9 +366,9 @@ export class SettingsWorkflow {
|
|
|
352
366
|
if (draft.kind === "set_endpoint" || draft.kind === "restore_endpoint")
|
|
353
367
|
return settingsIdentity({ ...common, target: this.options.resolveEndpoint(draft),
|
|
354
368
|
current: this.options.preferences.getLocalSettingSync(draft.endpoint === "grpc" ? "suiGrpcUrl" : "suiGraphqlUrl") });
|
|
355
|
-
if (draft.kind === "
|
|
369
|
+
if (draft.kind === "set_wallet_networks")
|
|
356
370
|
return settingsIdentity({ ...common,
|
|
357
|
-
current: this.
|
|
371
|
+
current: this.options.preferences.getLocalSettingSync("walletConnectionChains") });
|
|
358
372
|
if (draft.kind === "import_local_data" || draft.kind === "reset_local_data")
|
|
359
373
|
return settingsIdentity({ ...common,
|
|
360
374
|
userDataRevision: control.user_data_revision, file: operation.fileId ? this.records.file(operation.fileId) : null });
|
|
@@ -387,8 +401,8 @@ export class SettingsWorkflow {
|
|
|
387
401
|
return previous;
|
|
388
402
|
}
|
|
389
403
|
requireConfirmation(record, operation) {
|
|
390
|
-
if (["
|
|
391
|
-
throw new SettingsConflict("
|
|
404
|
+
if (["set_wallet_networks", "clear_read_address", "download_backup"].includes(operation.draft?.kind ?? ""))
|
|
405
|
+
throw new SettingsConflict("This action is applied by its explicit save or download request.");
|
|
392
406
|
const replacing = operation.draft?.kind === "restart_wallet_service" && typeof operation.preparation?.supersedesOperationId === "string"
|
|
393
407
|
? operation.preparation.supersedesOperationId : undefined;
|
|
394
408
|
this.records.requireAdmission(record, { action: "settings_confirm", operationId: operation.operationId,
|
|
@@ -469,8 +483,25 @@ export class SettingsWorkflow {
|
|
|
469
483
|
}
|
|
470
484
|
return file.sealed ? required("Check this backup again before restoring.") : { status: "receiving" };
|
|
471
485
|
}
|
|
472
|
-
|
|
486
|
+
expireInput(record) {
|
|
487
|
+
if (record.state.state !== "ready" || Date.parse(record.state.expiresAt) > this.now().getTime())
|
|
488
|
+
return record;
|
|
489
|
+
const operation = this.records.forCard(record);
|
|
490
|
+
const result = { outcome: "interrupted", recordedAt: this.now().toISOString(),
|
|
491
|
+
message: record.state.input.backupDelivery ? "This delivery card expired. Ask in chat for a new delivery card if the backup is still available."
|
|
492
|
+
: "The time to confirm this Settings change has ended." };
|
|
493
|
+
if (record.state.input.backupDelivery) {
|
|
494
|
+
if (!this.records.cards.replace(record, { ...record, state: { ...record.state, state: "closed", reason: "expired",
|
|
495
|
+
revision: record.state.revision + 1, data: this.records.resultView(operation, result) } }, this.now()))
|
|
496
|
+
throw new SettingsConflict("This delivery card changed before it could expire.");
|
|
497
|
+
}
|
|
498
|
+
else
|
|
499
|
+
this.records.finish(operation.operationId, result, undefined, "expired");
|
|
500
|
+
return this.records.cards.get(record.state.cardId);
|
|
501
|
+
}
|
|
502
|
+
async describe(record, currentOperation = false) {
|
|
473
503
|
this.options.assertCurrent();
|
|
504
|
+
record = this.expireInput(record);
|
|
474
505
|
let operation = this.records.forCard(record);
|
|
475
506
|
this.observeLocalSave(operation);
|
|
476
507
|
let downloadExpiryPending = false;
|
|
@@ -485,17 +516,13 @@ export class SettingsWorkflow {
|
|
|
485
516
|
if (operation.result === undefined) {
|
|
486
517
|
this.synchronize(operation.operationId);
|
|
487
518
|
operation = this.records.get(operation.operationId);
|
|
488
|
-
if (operation.checkpoint === "verify_export_after_owner_change" && !operation.result)
|
|
489
|
-
await this.verifyPriorExport(operation);
|
|
490
519
|
if (operation.draft?.kind === "restart_wallet_service" && operation.admittedAt) {
|
|
491
520
|
this.options.wallet.recoveryContext();
|
|
492
521
|
this.assertRecoveryRecorded();
|
|
493
522
|
}
|
|
494
523
|
this.records.transaction(() => {
|
|
495
|
-
record = this.
|
|
496
|
-
if (record.state.state === "closed" && record.state.reason === "expired") {
|
|
497
|
-
if (operation.fileId)
|
|
498
|
-
this.invalidateFile(operation.fileId);
|
|
524
|
+
record = this.expireInput(record);
|
|
525
|
+
if (!record.state.input.backupDelivery && record.state.state === "closed" && record.state.reason === "expired") {
|
|
499
526
|
this.records.finish(operation.operationId, { outcome: "interrupted", recordedAt: this.now().toISOString(), message: "The time to confirm this Settings change has ended." });
|
|
500
527
|
}
|
|
501
528
|
});
|
|
@@ -506,7 +533,7 @@ export class SettingsWorkflow {
|
|
|
506
533
|
}
|
|
507
534
|
catch (error) {
|
|
508
535
|
this.options.assertCurrent();
|
|
509
|
-
if (!operation.result)
|
|
536
|
+
if (!operation.result && !settingsDownload(operation)?.readyAt)
|
|
510
537
|
throw error;
|
|
511
538
|
downloadExpiryPending = true;
|
|
512
539
|
}
|
|
@@ -524,12 +551,14 @@ export class SettingsWorkflow {
|
|
|
524
551
|
const restoreCheck = operation.draft?.kind === "import_local_data" && !operation.result
|
|
525
552
|
? this.restoreCheck(operation, record, file) : undefined;
|
|
526
553
|
const allowed = [];
|
|
527
|
-
if (record.state.state === "ready" && record.ownerId === this.records.ownerId && operation.dataEpoch === this.records.control().data_epoch && !operation.result) {
|
|
554
|
+
if (!record.state.input.backupDelivery && record.state.state === "ready" && record.ownerId === this.records.ownerId && operation.dataEpoch === this.records.control().data_epoch && !operation.result) {
|
|
528
555
|
const fileIOAvailable = this.records.fileIOAvailable();
|
|
529
556
|
if (fileIOAvailable) {
|
|
530
557
|
allowed.push("settings_select", "settings_cancel");
|
|
531
558
|
if (this.records.control().active_operation_id === null)
|
|
532
|
-
allowed.push("settings_download_backup");
|
|
559
|
+
allowed.push("settings_download_backup", "settings_save_wallet_networks");
|
|
560
|
+
if (facts?.readAddress && facts.readConnectionId && this.records.control().active_operation_id === null)
|
|
561
|
+
allowed.push("settings_clear_read_address");
|
|
533
562
|
if (operation.draft)
|
|
534
563
|
allowed.push("settings_back");
|
|
535
564
|
}
|
|
@@ -544,7 +573,7 @@ export class SettingsWorkflow {
|
|
|
544
573
|
(file && !file.invalidated || this.records.canReserveFileIO(operation.operationId)))
|
|
545
574
|
allowed.push("settings_begin_upload");
|
|
546
575
|
}
|
|
547
|
-
else if (operation.phase !== "prepared" && operation.draft && !["
|
|
576
|
+
else if (operation.phase !== "prepared" && operation.draft && !["set_wallet_networks", "clear_read_address", "download_backup"].includes(operation.draft.kind) &&
|
|
548
577
|
(operation.draft.kind !== "restart_wallet_service" || this.canPrepareRestart()))
|
|
549
578
|
allowed.push("settings_prepare");
|
|
550
579
|
if (operation.phase === "uploading" && file && !file.invalidated && !file.sealed) {
|
|
@@ -570,32 +599,49 @@ export class SettingsWorkflow {
|
|
|
570
599
|
...(localSave.failure ? { failure: localSave.failure } : {}), ...(localSave.savedAt ? { savedAt: localSave.savedAt } : {}) } } : {}),
|
|
571
600
|
...(downloadEnd ? { endReason: downloadEnd } : {}),
|
|
572
601
|
...(storedDownload.expiresAt ? { expiresAt: storedDownload.expiresAt, remainingMs: Math.max(0, Date.parse(storedDownload.expiresAt) - this.now().getTime()) } : {}),
|
|
573
|
-
allowedFileActions: currentDownloadOwner && !this.closing ? [
|
|
574
|
-
...(storedDownload.readyAt ? ["settings_report_download"] : []),
|
|
575
|
-
...(!downloadEnd && file && !file.invalidated && localSave?.status !== "saving" ? [
|
|
576
|
-
...(
|
|
577
|
-
...(
|
|
578
|
-
|
|
602
|
+
allowedFileActions: currentDownloadOwner && record.state.state !== "closed" && !this.closing ? [
|
|
603
|
+
...(storedDownload.readyAt && currentAttempt?.cardId === record.state.cardId ? ["settings_report_download"] : []),
|
|
604
|
+
...(!downloadEnd && file && !file.invalidated && localSave?.status !== "saving" ? [
|
|
605
|
+
...(currentAttempt?.cardId === record.state.cardId ? ["settings_cancel_download"] : []),
|
|
606
|
+
...(record.state.state === "ready" && record.state.input.backupDelivery && !localSave && storedDownload.readyAt ? ["settings_retry_download"] : []),
|
|
607
|
+
...(record.state.state === "ready" && record.state.input.backupDelivery && localSave && (localSave.status === "needs_consent" || localSave.status === "failed" && localSave.failure === "not_published") &&
|
|
608
|
+
this.records.canReserveFileIO(operation.operationId) ? ["settings_store_backup"] : [])
|
|
609
|
+
] : [])
|
|
579
610
|
] : []
|
|
580
611
|
} : undefined;
|
|
581
612
|
if (storedDownload && !downloadExpiryPending)
|
|
582
613
|
this.scheduleDownloadExpiry();
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
614
|
+
const backupPath = localSave?.status === "saved" && file ? this.options.files.localBackupPath(file) : undefined;
|
|
615
|
+
const readingDownloadStatus = currentOperation && storedDownload !== undefined;
|
|
616
|
+
const activeDelivery = storedDownload && !currentOperation && record.state.state !== "closed";
|
|
617
|
+
const deliveryPending = storedDownload && (localSave?.status === "saving" || !downloadEnd &&
|
|
618
|
+
(!storedDownload.readyAt || !currentAttempt?.hostReport && !localSave));
|
|
619
|
+
const recordedResult = currentOperation && storedDownload
|
|
620
|
+
? (deliveryPending ? undefined : this.records.downloadResult(operation))
|
|
621
|
+
: operation.result;
|
|
622
|
+
const cleanup = recordedResult ? downloadExpiryPending ? "pending" : this.records.temporaryFileCleanup(operation.operationId) : undefined;
|
|
623
|
+
const data = { kind: "settings", operationId: operation.operationId,
|
|
624
|
+
phase: readingDownloadStatus ? recordedResult ? "terminal" : "executing"
|
|
625
|
+
: activeDelivery ? record.state.state === "ready" ? "draft" : "executing" : operation.phase, allowedActions: readingDownloadStatus ? [] : allowed,
|
|
626
|
+
observe: !readingDownloadStatus && (activeDelivery ? !storedDownload.readyAt || localSave?.status === "saving" : !operation.result && (["executing", "stopping", "starting"].includes(operation.phase) || restoreCheck?.status === "checking")),
|
|
588
627
|
...(restoreCheck ? { restoreCheck } : {}), ...(download ? { download } : {}),
|
|
589
628
|
...(typeof backupPath === "string" ? { backupLocation: { directory: dirname(backupPath), filename: basename(backupPath) } } : {}),
|
|
590
629
|
...(operation.draft ? { draft: operation.draft } : {}), ...(operation.preparationId ? { preparationId: operation.preparationId } : {}),
|
|
591
|
-
...(operation.preparation && !storedDownload ? { preparation: operation.preparation } : {}), ...(
|
|
592
|
-
...(cleanup ? { temporaryFileCleanup: cleanup } : {}),
|
|
630
|
+
...(operation.preparation && !storedDownload ? { preparation: operation.preparation } : {}), ...(recordedResult && !activeDelivery ? { result: recordedResult } : {}),
|
|
631
|
+
...(cleanup && !activeDelivery ? { temporaryFileCleanup: cleanup } : {}),
|
|
593
632
|
...(file ? { file: { fileId: file.fileId, displayName: file.displayName, totalSize: file.totalSize, receivedSize: file.receivedSize, sha256: file.sha256, sealed: file.sealed, cleanup: file.cleanup } } : {}),
|
|
594
633
|
...(facts ? { facts } : {}), ...(operation.priorRunId ? { priorRunId: operation.priorRunId } : {}),
|
|
595
634
|
...(operation.nextRunId ? { nextRunId: operation.nextRunId } : {}),
|
|
596
635
|
...(operation.supersedesOperationId ? { supersedesOperationId: operation.supersedesOperationId } : {}),
|
|
597
636
|
...(operation.supersededByOperationId ? { supersededByOperationId: operation.supersededByOperationId } : {}) };
|
|
598
637
|
assertNoForbiddenMcpFields(data);
|
|
638
|
+
if (!currentOperation && record.state.state === "closed" && record.state.data !== undefined)
|
|
639
|
+
return { record, data: settingsViewSchema.parse(record.state.data) };
|
|
640
|
+
if (!currentOperation && record.state.input.backupDelivery && downloadEnd) {
|
|
641
|
+
this.records.transaction(() => this.records.finishDownloadCard(operation.operationId, record.state.cardId));
|
|
642
|
+
record = this.records.cards.get(record.state.cardId);
|
|
643
|
+
return { record, data: settingsViewSchema.parse(record.state.data) };
|
|
644
|
+
}
|
|
599
645
|
return { record, data: settingsViewSchema.parse(data) };
|
|
600
646
|
}
|
|
601
647
|
async act(expected, action) {
|
|
@@ -613,8 +659,39 @@ export class SettingsWorkflow {
|
|
|
613
659
|
await this.continueDownload(expected, action);
|
|
614
660
|
return;
|
|
615
661
|
}
|
|
616
|
-
if (action.action === "
|
|
617
|
-
|
|
662
|
+
if (action.action === "settings_clear_read_address" || action.action === "settings_save_wallet_networks") {
|
|
663
|
+
const input = action;
|
|
664
|
+
if (stored.acceptedDigest === settingsIdentity(input))
|
|
665
|
+
return;
|
|
666
|
+
this.records.transaction(() => {
|
|
667
|
+
const { card, operation } = this.records.requireInput(expected, input.operationId);
|
|
668
|
+
this.records.requireFinalSlot();
|
|
669
|
+
const previous = input.action === "settings_save_wallet_networks" ? this.options.preferences.getWalletConnectionChainsSync() : undefined;
|
|
670
|
+
if (input.action === "settings_save_wallet_networks" && !isDeepStrictEqual(previous, input.expectedChains)) {
|
|
671
|
+
throw new SettingsConflict("The saved networks changed. Check the current settings before saving your selection.");
|
|
672
|
+
}
|
|
673
|
+
this.records.claimFinal(operation.operationId);
|
|
674
|
+
const next = { operationId: operation.operationId, cardId: operation.cardId, ownerId: operation.ownerId,
|
|
675
|
+
dataEpoch: operation.dataEpoch, phase: "executing", draft: input.action === "settings_clear_read_address"
|
|
676
|
+
? { kind: "clear_read_address" } : { kind: "set_wallet_networks", chains: input.chains },
|
|
677
|
+
acceptedDigest: settingsIdentity(input), admittedAt: this.now().toISOString() };
|
|
678
|
+
this.records.write(operation, next, card, { ...card, acceptedInput: input,
|
|
679
|
+
state: { ...card.state, state: "running", revision: card.state.revision + 1 } });
|
|
680
|
+
if (input.action === "settings_clear_read_address") {
|
|
681
|
+
this.records.clearReadAddress(input.connectionId, input.account);
|
|
682
|
+
this.records.finish(operation.operationId, { outcome: "completed", recordedAt: this.now().toISOString(),
|
|
683
|
+
message: "The saved read address was cleared. Your wallet connection and transaction requests are unchanged." });
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
this.options.preferences.setLocalSettingSync("walletConnectionChains", input.chains, this.now());
|
|
687
|
+
this.options.wallet.closeConnectionCardsWithChangedNetworks(input.chains);
|
|
688
|
+
this.records.finish(operation.operationId, { outcome: "completed", recordedAt: this.now().toISOString(),
|
|
689
|
+
message: "Wallet networks were saved. They apply to new connections; existing connections are unchanged.",
|
|
690
|
+
details: { walletConnectionChains: input.chains, previousWalletConnectionChains: previous, appliesTo: "new_connections" } });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
await this.cleanupTemporaryFiles(input.operationId);
|
|
694
|
+
return;
|
|
618
695
|
}
|
|
619
696
|
if (action.action === "settings_confirm" && stored.acceptedDigest === settingsIdentity(action))
|
|
620
697
|
return;
|
|
@@ -638,13 +715,12 @@ export class SettingsWorkflow {
|
|
|
638
715
|
await this.seal(expected, action);
|
|
639
716
|
return;
|
|
640
717
|
}
|
|
718
|
+
let selected;
|
|
641
719
|
this.records.transaction(() => {
|
|
642
720
|
const { card, operation } = this.records.requireInput(expected, "operationId" in action ? action.operationId : undefined);
|
|
643
721
|
if (!this.records.fileIOAvailable())
|
|
644
722
|
throw new SettingsConflict("Wait for the file transfer to finish before changing your selection.");
|
|
645
723
|
if (action.action === "settings_cancel") {
|
|
646
|
-
if (operation.fileId)
|
|
647
|
-
this.invalidateFile(operation.fileId);
|
|
648
724
|
this.records.finish(operation.operationId, { outcome: "interrupted", recordedAt: this.now().toISOString(), message: "This Settings change was cancelled. Nothing was applied." });
|
|
649
725
|
return;
|
|
650
726
|
}
|
|
@@ -658,29 +734,26 @@ export class SettingsWorkflow {
|
|
|
658
734
|
}
|
|
659
735
|
const next = this.withoutPreparation(operation);
|
|
660
736
|
if (action.action === "settings_select") {
|
|
661
|
-
if (operation.fileId)
|
|
662
|
-
this.invalidateFile(operation.fileId);
|
|
663
737
|
delete next.fileId;
|
|
664
738
|
delete next.preparation;
|
|
665
739
|
next.draft = action.draft;
|
|
666
740
|
next.phase = "draft";
|
|
667
741
|
}
|
|
668
742
|
this.records.write(operation, next, card);
|
|
743
|
+
if (action.action === "settings_select" && action.draft.kind !== "import_local_data")
|
|
744
|
+
selected = this.records.cards.get(card.state.cardId);
|
|
669
745
|
});
|
|
670
746
|
await this.cleanupTemporaryFiles(stored.operationId);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const file = this.records.file(id);
|
|
674
|
-
if (file?.role === "upload")
|
|
675
|
-
this.records.writeFile({ ...file, invalidated: true, cleanup: "pending" });
|
|
747
|
+
if (selected)
|
|
748
|
+
await this.prepare(selected, stored.operationId);
|
|
676
749
|
}
|
|
677
750
|
async prepare(expected, id) {
|
|
678
751
|
const initial = this.records.requireInput(expected, id);
|
|
679
752
|
const draft = initial.operation.draft;
|
|
680
753
|
if (!draft)
|
|
681
754
|
throw new SettingsConflict("Choose what you want to change in Settings first.");
|
|
682
|
-
if (["
|
|
683
|
-
throw new SettingsConflict("
|
|
755
|
+
if (["set_wallet_networks", "clear_read_address", "download_backup"].includes(draft.kind))
|
|
756
|
+
throw new SettingsConflict("This action is applied by its explicit save or download request.");
|
|
684
757
|
let preparation;
|
|
685
758
|
if (draft.kind === "import_local_data") {
|
|
686
759
|
await this.checkImport(expected, id);
|
|
@@ -746,8 +819,6 @@ export class SettingsWorkflow {
|
|
|
746
819
|
this.records.write(operation, { ...this.withoutPreparation(operation), phase: "uploading" }, card);
|
|
747
820
|
return;
|
|
748
821
|
}
|
|
749
|
-
if (operation.fileId)
|
|
750
|
-
this.invalidateFile(operation.fileId);
|
|
751
822
|
const fileId = randomUUID();
|
|
752
823
|
file = { fileId, operationId: operation.operationId, role: "upload", relativePath: `.settings-staging/${fileId}.upload`, displayName: action.name,
|
|
753
824
|
totalSize: action.totalSize, receivedSize: 0, sha256: action.sha256, sealed: false, invalidated: false, cleanup: "pending" };
|
|
@@ -923,8 +994,8 @@ export class SettingsWorkflow {
|
|
|
923
994
|
const initial = this.records.requireInput(expected, action.operationId), draft = initial.operation.draft;
|
|
924
995
|
if (!draft || initial.operation.preparationId !== action.preparationId)
|
|
925
996
|
throw new SettingsConflict("Review the current Settings change before confirming it.");
|
|
926
|
-
if (
|
|
927
|
-
throw new SettingsConflict("
|
|
997
|
+
if (draft.kind === "download_backup" || draft.kind === "set_wallet_networks" || draft.kind === "clear_read_address")
|
|
998
|
+
throw new SettingsConflict("This action is applied by its explicit save or download request.");
|
|
928
999
|
if (draft.kind === "restart_wallet_service") {
|
|
929
1000
|
this.restart(expected, action);
|
|
930
1001
|
return;
|
|
@@ -997,15 +1068,12 @@ export class SettingsWorkflow {
|
|
|
997
1068
|
effectiveValueAtSave: prepared.effectiveValue, effectiveSourceAtSave: prepared.source,
|
|
998
1069
|
appliesAfter: SETTINGS_APPLIES_AFTER_RESTART };
|
|
999
1070
|
}
|
|
1000
|
-
else if (draft.kind === "clear_read_address")
|
|
1001
|
-
this.options.activity.clearActiveAccountSync(this.now());
|
|
1002
1071
|
else if (replacement)
|
|
1003
1072
|
details = this.options.localData.applyValidatedReplacement(imported, decisionTime, expected.state.cardId);
|
|
1004
1073
|
this.records.finish(operation.operationId, { outcome: "completed", recordedAt: this.now().toISOString(),
|
|
1005
1074
|
message: endpoint ? "The network address was saved. This result describes the settings at save time."
|
|
1006
|
-
: draft.kind === "
|
|
1007
|
-
:
|
|
1008
|
-
: "Local data was reset. This does not cancel a transaction on Sui or disconnect your wallet app.", details });
|
|
1075
|
+
: draft.kind === "import_local_data" ? "Local records were replaced. Restart the apps using Sui MCP before continuing."
|
|
1076
|
+
: "Local data was reset. This does not cancel a transaction on Sui or disconnect your wallet app.", details });
|
|
1009
1077
|
});
|
|
1010
1078
|
if (refused)
|
|
1011
1079
|
throw refused;
|
|
@@ -1088,12 +1156,18 @@ export class SettingsWorkflow {
|
|
|
1088
1156
|
if (retained.downloadReadyAt)
|
|
1089
1157
|
this.records.recordDownloadReady(id, retained.downloadReadyAt, DOWNLOAD_RETRY_WINDOW_MS);
|
|
1090
1158
|
else if (this.records.get(id)?.draft?.kind === "download_backup" && retained.result.outcome !== "completed")
|
|
1091
|
-
this.records.endDownload(id, "source_unavailable");
|
|
1159
|
+
this.records.endDownload(id, "source_unavailable", retained.result);
|
|
1092
1160
|
const fileId = retained.file?.fileId ?? this.records.get(id)?.fileId;
|
|
1093
1161
|
if (fileId && this.finishedFileIO.has(fileId)) {
|
|
1094
1162
|
this.records.releaseFileIO(fileId);
|
|
1095
1163
|
settledFileId = fileId;
|
|
1096
1164
|
}
|
|
1165
|
+
if (retained.downloadReadyAt) {
|
|
1166
|
+
this.records.releaseFinal(id);
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
if (this.records.get(id)?.draft?.kind === "download_backup")
|
|
1170
|
+
return true;
|
|
1097
1171
|
return this.records.finish(id, retained.result);
|
|
1098
1172
|
});
|
|
1099
1173
|
if (settledFileId)
|
|
@@ -30,11 +30,11 @@ export class SqliteCardRecordStore {
|
|
|
30
30
|
let record = this.get(expected.state.cardId);
|
|
31
31
|
if (!record || record.tokenHash !== expected.tokenHash || record.ownerId !== expected.ownerId)
|
|
32
32
|
throw new Error("Saved card access is unavailable.");
|
|
33
|
-
if (record.state.state === "ready" && Date.parse(record.state.expiresAt) <= now.getTime()) {
|
|
33
|
+
if (!["connect", "disconnect", "review", "settings"].includes(record.state.kind) && record.state.state === "ready" && Date.parse(record.state.expiresAt) <= now.getTime()) {
|
|
34
34
|
const next = { ...record, state: { ...record.state, state: "closed", reason: "expired", revision: record.state.revision + 1 } };
|
|
35
|
-
if (!this.replace(record, next))
|
|
35
|
+
if (!this.replace(record, next, now))
|
|
36
36
|
throw new Error("Card expiry could not be committed.");
|
|
37
|
-
record =
|
|
37
|
+
record = this.get(record.state.cardId);
|
|
38
38
|
}
|
|
39
39
|
return { record, evaluatedAt: now.toISOString() };
|
|
40
40
|
}).immediate();
|
|
@@ -53,25 +53,45 @@ export class SqliteCardRecordStore {
|
|
|
53
53
|
}).immediate();
|
|
54
54
|
}
|
|
55
55
|
create(record) {
|
|
56
|
+
if (record.state.state === "closed")
|
|
57
|
+
record = this.terminal(record);
|
|
56
58
|
this.db.prepare(`INSERT INTO live_read_cards
|
|
57
59
|
(id, owner_id, token_hash, kind, state, reason, error, revision, created_at, expires_at,
|
|
58
60
|
input_json, accepted_input_json, result_json, receipt_display_json, scope, operation_id)
|
|
59
61
|
VALUES (@id, @owner, @token, @kind, @state, @reason, @error, @revision, @created, @expires,
|
|
60
62
|
@input, @accepted, @result, @display, @scope, @operation)`).run(values(record));
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
terminal(record) {
|
|
65
|
+
if (["connect", "disconnect", "review", "settings"].includes(record.state.kind) && record.state.data === undefined) {
|
|
66
|
+
throw new Error("The owning domain must supply the card's committed result.");
|
|
67
|
+
}
|
|
68
|
+
return record;
|
|
69
|
+
}
|
|
70
|
+
replace(expected, next, _at) {
|
|
71
|
+
if (expected.state.state === "closed")
|
|
72
|
+
return false;
|
|
73
|
+
return this.db.transaction(() => {
|
|
74
|
+
if (next.state.state === "closed")
|
|
75
|
+
next = this.terminal(next);
|
|
76
|
+
return this.db.prepare(`UPDATE live_read_cards SET owner_id=@owner,state=@state,reason=@reason,error=@error,
|
|
77
|
+
revision=@revision,input_json=@input,accepted_input_json=@accepted,result_json=@result,
|
|
78
|
+
receipt_display_json=@display,scope=@scope,operation_id=@operation
|
|
79
|
+
WHERE id=@id AND owner_id=@expectedOwner AND token_hash=@token AND revision=@expectedRevision
|
|
80
|
+
AND state=@expectedState`).run({ ...values(next), expectedOwner: expected.ownerId,
|
|
81
|
+
expectedRevision: expected.state.revision, expectedState: expected.state.state }).changes === 1;
|
|
82
|
+
}).immediate();
|
|
69
83
|
}
|
|
70
84
|
recover(ownerId, now) {
|
|
71
|
-
this.db.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
this.db.transaction(() => {
|
|
86
|
+
const ids = this.db.prepare("SELECT id FROM live_read_cards WHERE state!='closed' AND kind NOT IN ('connect','disconnect','review','settings') AND owner_id!=?").all(ownerId);
|
|
87
|
+
for (const { id } of ids) {
|
|
88
|
+
const record = this.get(id);
|
|
89
|
+
const reason = record.state.state === "ready" && Date.parse(record.state.expiresAt) <= now.getTime() ? "expired" : "server_restarted";
|
|
90
|
+
if (!this.replace(record, { ...record, ownerId, state: { ...record.state, state: "closed", reason, revision: record.state.revision + 1 } }, now)) {
|
|
91
|
+
throw new Error("Card recovery could not be committed.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}).immediate();
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
function values(record) {
|
|
@@ -5,11 +5,11 @@ import { INITIAL_CHAIN_OBSERVATION_STATUSES } from "./transactionRequest.js";
|
|
|
5
5
|
export function insertLiveReviewSessionRow(db, session) {
|
|
6
6
|
const serialized = serializeSessionForLiveReviewSessionRow(session);
|
|
7
7
|
db.prepare(`INSERT INTO live_review_sessions
|
|
8
|
-
(id, token_hash, status, account, owner_id, review_revision, preparation_id, preparation_error, wallet_connection_id,
|
|
8
|
+
(id, token_hash, status, account, owner_id, review_revision, preparation_id, preparation_error, wallet_connection_id, wallet_grant_revision, current_attempt_id,
|
|
9
9
|
plans_json, review_state_json,
|
|
10
10
|
created_at, expires_at, last_activity_at, revision, write_contract_version)
|
|
11
11
|
VALUES
|
|
12
|
-
(@id, @tokenHash, @status, @account, @ownerId, @reviewRevision, @preparationId, @preparationError, @walletConnectionId, @
|
|
12
|
+
(@id, @tokenHash, @status, @account, @ownerId, @reviewRevision, @preparationId, @preparationError, @walletConnectionId, @walletGrantRevision, @currentAttemptId,
|
|
13
13
|
@plansJson, @reviewStateJson,
|
|
14
14
|
@createdAt, @expiresAt, @lastActivityAt, 0, @writeContractVersion)`).run(serialized);
|
|
15
15
|
}
|
|
@@ -25,7 +25,7 @@ export function updateLiveReviewSessionRow(db, expectedRevision, session) {
|
|
|
25
25
|
preparation_id = @preparationId,
|
|
26
26
|
preparation_error = @preparationError,
|
|
27
27
|
wallet_connection_id = @walletConnectionId,
|
|
28
|
-
|
|
28
|
+
wallet_grant_revision = @walletGrantRevision,
|
|
29
29
|
current_attempt_id = @currentAttemptId,
|
|
30
30
|
plans_json = @plansJson,
|
|
31
31
|
review_state_json = @reviewStateJson,
|
|
@@ -133,7 +133,7 @@ export function sessionFromLiveReviewSessionRow(row) {
|
|
|
133
133
|
ownerId: row.owner_id,
|
|
134
134
|
reviewRevision: row.review_revision,
|
|
135
135
|
...(row.wallet_connection_id === null ? {} : { walletConnectionId: row.wallet_connection_id }),
|
|
136
|
-
...(row.
|
|
136
|
+
...(row.wallet_grant_revision === null ? {} : { walletGrantRevision: row.wallet_grant_revision }),
|
|
137
137
|
...(row.preparation_error === null ? {} : { preparationError: row.preparation_error }),
|
|
138
138
|
...(row.preparation_id === null ? {} : { preparationId: row.preparation_id }),
|
|
139
139
|
...(row.current_attempt_id === null ? {} : { currentAttemptId: row.current_attempt_id })
|
|
@@ -150,7 +150,7 @@ export function serializeSessionForLiveReviewSessionRow(session) {
|
|
|
150
150
|
preparationId: session.preparationId ?? null,
|
|
151
151
|
preparationError: session.preparationError ?? null,
|
|
152
152
|
walletConnectionId: session.walletConnectionId ?? null,
|
|
153
|
-
|
|
153
|
+
walletGrantRevision: session.walletGrantRevision ?? null,
|
|
154
154
|
currentAttemptId: session.currentAttemptId ?? null,
|
|
155
155
|
plansJson: JSON.stringify(session.plans),
|
|
156
156
|
reviewStateJson: session.reviewState ? JSON.stringify(session.reviewState) : null,
|