@zktx.io/sui-mcp 0.5.0 → 0.6.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.
Files changed (138) hide show
  1. package/README.md +21 -21
  2. package/dist/adapters/adapterPromptSurfaces.js +0 -20
  3. package/dist/adapters/deepbook/deepbookQuotePolicy.js +0 -4
  4. package/dist/adapters/deepbook/deepbookReviewEvidence.js +0 -9
  5. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +0 -9
  6. package/dist/adapters/intentPlanFactories.js +0 -6
  7. package/dist/core/action/contractNameRegistry.js +0 -53
  8. package/dist/core/action/forbiddenFields.js +0 -4
  9. package/dist/core/action/ptbVisualizationProducer.js +0 -3
  10. package/dist/core/action/receiptPtbGraph.js +0 -17
  11. package/dist/core/action/reviewTimeSimulationEvidence.js +0 -4
  12. package/dist/core/action/signableAdapterContract.js +0 -5
  13. package/dist/core/action/suiChainReceiptMapping.js +0 -18
  14. package/dist/core/action/suiChainReceiptReader.js +1 -21
  15. package/dist/core/action/suiChainReceiptVerifier.js +0 -10
  16. package/dist/core/action/transactionFunding.js +0 -7
  17. package/dist/core/action/types.js +0 -1
  18. package/dist/core/action/walletReviewContractAssembler.js +0 -2
  19. package/dist/core/activity/localDataService.js +65 -52
  20. package/dist/core/activity/localDataValidation.js +1 -3
  21. package/dist/core/activity/sqliteActivityStore.js +24 -24
  22. package/dist/core/activity/sqliteActivityStoreRows.js +0 -2
  23. package/dist/core/activity/sqliteActivityStoreSchema.js +81 -14
  24. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  25. package/dist/core/read/deepbookReadHelpers.js +0 -2
  26. package/dist/core/read/deepbookRegistry.js +0 -19
  27. package/dist/core/read/deepbookUsdcChartService.js +0 -2
  28. package/dist/core/read/readCardResult.js +0 -2
  29. package/dist/core/read/readService.js +0 -19
  30. package/dist/core/read/readServiceTypes.js +0 -2
  31. package/dist/core/session/cardSession.js +1 -5
  32. package/dist/core/session/cardSessionStore.js +54 -7
  33. package/dist/core/session/reviewValidity.js +0 -3
  34. package/dist/core/session/sessionRecordStore.js +0 -1
  35. package/dist/core/session/sessionStore.js +0 -32
  36. package/dist/core/session/settingsFileIdentity.js +9 -0
  37. package/dist/core/session/settingsFileProtocol.js +20 -0
  38. package/dist/core/session/settingsFileStore.js +337 -0
  39. package/dist/core/session/settingsInputError.js +32 -0
  40. package/dist/core/session/settingsOperation.js +220 -0
  41. package/dist/core/session/settingsPreparationSql.js +13 -0
  42. package/dist/core/session/settingsWorkflow.js +1183 -0
  43. package/dist/core/session/sqliteCardStore.js +0 -2
  44. package/dist/core/session/sqliteSessionStore.js +0 -51
  45. package/dist/core/session/sqliteSettingsWorkflowStore.js +453 -0
  46. package/dist/core/session/sqliteTransactionMaterialStore.js +0 -11
  47. package/dist/core/session/sqliteWalletWorkflowStore.js +160 -66
  48. package/dist/core/session/status.js +2 -4
  49. package/dist/core/session/transactionMaterialStore.js +0 -4
  50. package/dist/core/session/transactionRequest.js +0 -4
  51. package/dist/core/session/walletConnection.js +26 -10
  52. package/dist/core/session/walletWorkflow.js +70 -30
  53. package/dist/core/session/workflowState.js +14 -14
  54. package/dist/core/session/workflowView.js +18 -8
  55. package/dist/core/suiAddress.js +0 -5
  56. package/dist/core/suiEndpoint.js +0 -3
  57. package/dist/mcp/prompts.js +0 -12
  58. package/dist/mcp/responseGuidance.js +10 -5
  59. package/dist/mcp/schemas.js +0 -3
  60. package/dist/mcp/serverInfo.js +3 -4
  61. package/dist/mcp/toolNames.js +2 -2
  62. package/dist/mcp/tools/account/index.js +1 -19
  63. package/dist/mcp/tools/action/prepareSuiActionReview.js +0 -6
  64. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  65. package/dist/mcp/tools/read/serverStatusTools.js +0 -1
  66. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  67. package/dist/mcp/tools/session/shared.js +1 -2
  68. package/dist/mcp/tools/session/statusTools.js +4 -5
  69. package/dist/mcp/tools/settings/index.js +8 -28
  70. package/dist/mcp-app/account.html +14 -14
  71. package/dist/mcp-app/account.notices.txt +1 -1
  72. package/dist/mcp-app/chart.html +9 -9
  73. package/dist/mcp-app/chart.notices.txt +1 -1
  74. package/dist/mcp-app/connect.html +14 -14
  75. package/dist/mcp-app/connect.notices.txt +1 -1
  76. package/dist/mcp-app/receipt.html +1298 -1463
  77. package/dist/mcp-app/receipt.notices.txt +226 -1204
  78. package/dist/mcp-app/review.html +1298 -1463
  79. package/dist/mcp-app/review.notices.txt +226 -1204
  80. package/dist/mcp-app/settings.html +3133 -0
  81. package/dist/mcp-app/settings.notices.txt +973 -0
  82. package/dist/mcp-ui/contracts.js +8 -2
  83. package/dist/mcp-ui/readCards.js +1 -1
  84. package/dist/mcp-ui/tools.js +34 -4
  85. package/dist/review-server/server.js +3 -51
  86. package/dist/runtime/application.js +41 -14
  87. package/dist/runtime/config.js +0 -2
  88. package/dist/runtime/localSettingsService.js +23 -2
  89. package/dist/runtime/logger.js +0 -2
  90. package/dist/runtime/reviewServerAcquire.js +1 -18
  91. package/dist/runtime/runtimeTermination.js +1 -4
  92. package/dist/runtime/shared/authenticatedFetch.js +2 -4
  93. package/dist/runtime/shared/control.js +4 -7
  94. package/dist/runtime/shared/dataAccess.js +0 -2
  95. package/dist/runtime/shared/http.js +14 -0
  96. package/dist/runtime/shared/mcpHttp.js +6 -6
  97. package/dist/runtime/shared/ownerLease.js +0 -4
  98. package/dist/runtime/shared/server.js +1 -2
  99. package/dist/runtime/shared/stdio.js +2 -5
  100. package/dist/runtime/smokeMainnetRead.js +0 -3
  101. package/dist/runtime/smokeReview.js +0 -4
  102. package/dist/runtime/start.js +0 -2
  103. package/dist/runtime/suiTransactionGraphqlMapping.js +0 -2
  104. package/dist/runtime/suiTransactionGraphqlQueries.js +0 -3
  105. package/dist/runtime/walletConnectConfig.js +0 -2
  106. package/dist/runtime/walletConnectStorage.js +2 -10
  107. package/dist/runtime/walletConnectTransport.js +0 -11
  108. package/dist/runtime/walletSdkChildRuntime.js +2 -6
  109. package/dist/runtime/walletSdkIpc.js +2 -1
  110. package/dist/runtime/walletSdkProcess.js +3 -8
  111. package/docs/AGENT_BEHAVIOR.md +45 -15
  112. package/docs/LOCAL_DB_ARCHITECTURE.md +145 -22
  113. package/docs/MCP_SETUP.md +99 -39
  114. package/docs/MCP_TOOLS.md +288 -24
  115. package/docs/SDK_API.md +63 -10
  116. package/docs/UTILITY_INDEX.md +9 -6
  117. package/docs/WALLET_CONNECTION.md +32 -10
  118. package/package.json +22 -8
  119. package/dist/core/activity/schemaVersion.js +0 -2
  120. package/dist/core/session/keyedRecordStore.js +0 -18
  121. package/dist/core/session/settingsSession.js +0 -1
  122. package/dist/core/session/settingsSessions.js +0 -45
  123. package/dist/review-app/brand-dark.svg +0 -1
  124. package/dist/review-app/brand-light.svg +0 -1
  125. package/dist/review-app/favicon.svg +0 -1
  126. package/dist/review-app/settings.css +0 -1
  127. package/dist/review-app/settings.js +0 -1
  128. package/dist/review-app/ui.css +0 -809
  129. package/dist/review-server/assets.js +0 -48
  130. package/dist/review-server/html.js +0 -45
  131. package/dist/review-server/http.js +0 -47
  132. package/dist/review-server/middleware/reviewToken.js +0 -7
  133. package/dist/review-server/settingsApi.js +0 -158
  134. package/docs/AGENT_DEVELOPMENT_POLICY.md +0 -815
  135. package/docs/FRONTEND_POLICY.md +0 -534
  136. package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +0 -166
  137. package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +0 -161
  138. package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +0 -259
@@ -10,7 +10,7 @@ import { reviewStateOutputSchema } from "../action/schemas.js";
10
10
  import { LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION } from "./liveReviewSessionContract.js";
11
11
  import { SqliteCardRecordStore } from "./sqliteCardStore.js";
12
12
  import { receiptDisplaySchema } from "./cardSession.js";
13
- import { isOwnedConnectedWallet, walletConnectionSelection, walletConnectionSchema, SUI_MAINNET_WALLET_CHAIN, SUI_SIGN_TRANSACTION_METHOD } from "./walletConnection.js";
13
+ import { isOwnedConnectedWallet, walletConnectionSelection, walletConnectionSchema, walletRecoveryImpactSchema, SUI_MAINNET_WALLET_CHAIN, SUI_SIGN_TRANSACTION_METHOD } from "./walletConnection.js";
14
14
  import { SIGNATURE_WAIT_MS, CHAIN_RECEIPT_LOOKUP_MAX_AGE_MS, INITIAL_CHAIN_OBSERVATION_STATUSES, isInitialChainObservation, transactionRequestSchema, transactionExecutionSummarySchema, requestTransitions } from "./transactionRequest.js";
15
15
  export class WorkflowConflict extends Error {
16
16
  events;
@@ -24,16 +24,16 @@ const interruptionReasons = {
24
24
  wallet_change: "The wallet connection changed or became unavailable before submission. Nothing was submitted.",
25
25
  connection_conflict: "More than one wallet connection is present. Nothing was submitted.",
26
26
  disconnect_requested: "Wallet disconnection was requested before submission. Nothing was submitted.",
27
+ wallet_service_restarted: "The wallet service was restarted before submission. Nothing was submitted.",
27
28
  wallet_service_lost: "The wallet service stopped before submission. Nothing was submitted."
28
29
  };
29
30
  const preparationInterruptionReasons = {
30
31
  wallet_change: "The selected wallet connection changed.",
31
32
  connection_conflict: "More than one wallet connection is present. Disconnect the connections you no longer need, then update this review.",
32
33
  disconnect_requested: "Disconnection of the selected wallet was requested. Connect a wallet before updating this review.",
34
+ wallet_service_restarted: "The wallet service was restarted. Update this review before requesting approval.",
33
35
  wallet_service_lost: "The wallet service stopped. Restore the wallet connection and update this review before requesting approval."
34
36
  };
35
- /** Purpose-specific mutations on the activity owner's connection. Each method
36
- * commits card, request and public history together; no network work runs here. */
37
37
  export class SqliteWalletWorkflowStore {
38
38
  db;
39
39
  ownerId;
@@ -42,8 +42,16 @@ export class SqliteWalletWorkflowStore {
42
42
  materialStore;
43
43
  evaluateState;
44
44
  readActiveAccount;
45
+ readSdkSnapshot;
45
46
  cards;
46
- constructor(db, ownerId, writeActiveAccount, clock, materialStore, evaluateState, readActiveAccount) {
47
+ runId;
48
+ setWalletRunId(runId) { this.runId = runId; }
49
+ assertWalletInputAllowed() {
50
+ if (this.db.prepare("SELECT wallet_input_blocked FROM settings_control WHERE id=1").get().wallet_input_blocked) {
51
+ throw new WorkflowConflict("Wallet input is blocked while the connection service is being recovered.");
52
+ }
53
+ }
54
+ constructor(db, ownerId, writeActiveAccount, clock, materialStore, evaluateState, readActiveAccount, readSdkSnapshot) {
47
55
  this.db = db;
48
56
  this.ownerId = ownerId;
49
57
  this.writeActiveAccount = writeActiveAccount;
@@ -51,9 +59,10 @@ export class SqliteWalletWorkflowStore {
51
59
  this.materialStore = materialStore;
52
60
  this.evaluateState = evaluateState;
53
61
  this.readActiveAccount = readActiveAccount;
62
+ this.readSdkSnapshot = readSdkSnapshot;
54
63
  this.cards = new SqliteCardRecordStore(db);
55
64
  }
56
- evaluate(input) { return this.evaluateState(input); }
65
+ reconcileAndEvaluate(input) { return this.evaluateState(input); }
57
66
  currentCard(expected) {
58
67
  const current = this.cards.get(expected.state.cardId);
59
68
  if (!current || current.tokenHash !== expected.tokenHash || current.scope !== expected.scope || current.ownerId !== expected.ownerId ||
@@ -92,17 +101,32 @@ export class SqliteWalletWorkflowStore {
92
101
  return recovery;
93
102
  }
94
103
  recordWalletFailure(input) {
95
- // Retried by the workflow until revocation is durably recorded. The SDK
96
- // run is already fenced, so a failed write cannot preserve usable authority.
97
- this.invalidateWalletRun("wallet_service_lost", new Date(input.observedAt));
104
+ this.invalidateWalletRun("wallet_service_lost", new Date(input.observedAt), input.runId);
98
105
  }
99
106
  publishWalletState() { this.publishAvailableWallets(); }
100
- applyWalletObservation(snapshot, previous, ready) {
107
+ applyWalletObservation(snapshot, previous, ready, recordReady) {
101
108
  this.db.transaction(() => {
102
109
  for (const record of this.connections()) {
103
- if (!record.topic || record.connection.status !== "connected")
110
+ if (!record.topic)
104
111
  continue;
105
112
  const observed = snapshot.sessions.find((item) => item.topic === record.topic);
113
+ if (record.connection.status !== "connected") {
114
+ if ((ready || record.ownerId === this.ownerId && record.runId === snapshot.runId) &&
115
+ observed?.status === "present" && !record.sdkPending &&
116
+ !this.pendingDisconnect(record.connection.connectionId, record.ownerId)) {
117
+ const transferred = record.ownerId !== this.ownerId || record.runId !== snapshot.runId;
118
+ const changed = !isDeepStrictEqual(record.connection.accounts, observed.session.accounts) ||
119
+ !isDeepStrictEqual(record.connection.methods, observed.session.methods) ||
120
+ record.connection.expiresAt !== observed.session.expiresAt || record.connection.walletName !== observed.session.walletName;
121
+ if (transferred)
122
+ this.db.prepare("UPDATE live_wallet_connections SET owner_id=?,run_id=? WHERE id=?")
123
+ .run(this.ownerId, snapshot.runId, record.connection.connectionId);
124
+ if (transferred || changed)
125
+ this.updateConnection(record.connection.connectionId, { accounts: observed.session.accounts,
126
+ methods: observed.session.methods, expiresAt: observed.session.expiresAt, walletName: observed.session.walletName }, this.clock());
127
+ }
128
+ continue;
129
+ }
106
130
  if (ready && observed?.status === "present" && !record.sdkPending && !this.pendingDisconnect(record.connection.connectionId, record.ownerId)) {
107
131
  this.restoreConnection(observed.session, this.clock());
108
132
  }
@@ -118,25 +142,38 @@ export class SqliteWalletWorkflowStore {
118
142
  }
119
143
  }
120
144
  this.invalidateConflictingConnections(this.clock());
145
+ if (ready)
146
+ recordReady?.();
121
147
  if (ready)
122
148
  this.publishAvailableWallets();
123
149
  }).immediate();
124
150
  }
125
- invalidateWalletRun(cause, now = this.clock()) {
151
+ invalidateWalletRun(cause, now = this.clock(), runId) {
126
152
  this.db.transaction(() => {
153
+ const targets = new Set();
154
+ if (runId !== undefined) {
155
+ for (const row of this.db.prepare("SELECT connection_id FROM live_request_authority WHERE owner_id=? AND run_id=?")
156
+ .all(this.ownerId, runId))
157
+ targets.add(row.connection_id);
158
+ for (const row of this.db.prepare("SELECT wallet_connection_id FROM live_review_sessions WHERE owner_id=? AND preparation_run_id=? AND wallet_connection_id IS NOT NULL")
159
+ .all(this.ownerId, runId))
160
+ targets.add(row.wallet_connection_id);
161
+ }
127
162
  for (const record of this.connections()) {
128
- if (record.ownerId !== this.ownerId)
163
+ if (record.ownerId !== this.ownerId || runId !== undefined && record.runId !== runId)
129
164
  continue;
130
- this.invalidateConnectionRequests(record.connection.connectionId, now, cause);
165
+ targets.add(record.connection.connectionId);
131
166
  if (["connected", "awaiting_approval"].includes(record.connection.status))
132
167
  this.updateConnection(record.connection.connectionId, {}, now);
133
168
  }
169
+ for (const id of targets)
170
+ this.invalidateConnectionRequests(id, now, cause, runId);
134
171
  this.publishAvailableWallets();
135
172
  }).immediate();
136
173
  }
137
174
  connection(id) {
138
175
  const row = this.db.prepare("SELECT * FROM live_wallet_connections WHERE id=?").get(id);
139
- return row ? { connection: walletConnectionSchema.parse(JSON.parse(row.connection_json)), ownerId: row.owner_id,
176
+ return row ? { connection: walletConnectionSchema.parse(JSON.parse(row.connection_json)), ownerId: row.owner_id, ...(row.run_id === null ? {} : { runId: row.run_id }),
140
177
  sdkPending: row.sdk_pending === 1, ...(row.topic === null ? {} : { topic: row.topic }) } : undefined;
141
178
  }
142
179
  connections() {
@@ -146,20 +183,47 @@ export class SqliteWalletWorkflowStore {
146
183
  connectionView(connection) {
147
184
  return { ...connection, ...(this.pendingDisconnect(connection.connectionId) ? { pendingAction: "disconnect" } : {}) };
148
185
  }
149
- connectionViews() {
150
- return this.connections().filter((item) => item.ownerId === this.ownerId).map((item) => this.connectionView(item.connection));
186
+ connectionPresence(records, snapshot) {
187
+ const pendingConnectionIds = records.filter((item) => item.ownerId === this.ownerId &&
188
+ (item.sdkPending || !!this.pendingDisconnect(item.connection.connectionId))).map((item) => item.connection.connectionId);
189
+ if (!snapshot || snapshot.runId !== this.runId)
190
+ return { confirmed: false, connections: [], unrecorded: false, pendingConnectionIds };
191
+ const connections = [];
192
+ let unrecorded = false;
193
+ for (const session of snapshot.sessions) {
194
+ if (session.status !== "present")
195
+ continue;
196
+ const record = records.find((item) => item.topic === session.topic && item.ownerId === this.ownerId && item.runId === this.runId);
197
+ if (record)
198
+ connections.push({ connectionId: record.connection.connectionId, expiresAt: session.session.expiresAt });
199
+ else
200
+ unrecorded = true;
201
+ }
202
+ return { confirmed: true, connections, unrecorded, pendingConnectionIds };
203
+ }
204
+ readConnectionFacts(now) {
205
+ return this.db.transaction(() => {
206
+ const records = this.connections(), snapshot = this.readSdkSnapshot(), account = this.readActiveAccount();
207
+ return { evaluatedAt: now.toISOString(), activeAccount: account?.address, activeAccountConnectionId: account?.walletId,
208
+ connections: records.filter((item) => item.ownerId === this.ownerId).map((item) => this.connectionView(item.connection)),
209
+ connectionPresence: this.connectionPresence(records, snapshot),
210
+ ...(snapshot ? { walletObservation: { runId: snapshot.runId, sequence: snapshot.sequence, observedAt: snapshot.observedAt } } : {}) };
211
+ }).deferred();
151
212
  }
152
213
  connectionSelection(now = this.clock()) {
153
- return walletConnectionSelection(this.connectionViews(), now.getTime(), this.readActiveAccount());
214
+ const facts = this.readConnectionFacts(now);
215
+ return walletConnectionSelection(facts.connections, now.getTime(), facts.connectionPresence, facts.activeAccount ? { address: facts.activeAccount, walletId: facts.activeAccountConnectionId } : undefined);
154
216
  }
155
217
  isUsableConnection(id, now = this.clock()) {
156
218
  return this.connectionSelection(now).connection?.connectionId === id;
157
219
  }
158
220
  invalidateConflictingConnections(now) {
159
- const conflict = this.connectionSelection(now).conflict;
160
- if (conflict)
161
- for (const id of conflict.connectionIds)
162
- this.invalidateConnectionRequests(id, now, "connection_conflict");
221
+ const selection = this.connectionSelection(now);
222
+ if (selection.conflict || selection.connectionReconciliationRequired)
223
+ for (const record of this.connections()) {
224
+ if (record.ownerId === this.ownerId)
225
+ this.invalidateConnectionRequests(record.connection.connectionId, now, selection.conflict ? "connection_conflict" : "wallet_change");
226
+ }
163
227
  }
164
228
  settleConnection(id, source) {
165
229
  this.db.transaction(() => {
@@ -167,15 +231,15 @@ export class SqliteWalletWorkflowStore {
167
231
  if (!card || card.ownerId !== this.ownerId || card.operationId !== id || card.acceptedInput?.action !== source.action ||
168
232
  card.acceptedInput.walletRunId !== source.runId)
169
233
  return;
170
- // A connection may progress from pairing to one admitted disconnect.
171
- // That later operation owns sdk_pending even if its result is already
172
- // visible and the original pairing's failed cleanup write arrives late.
173
234
  const disconnect = this.db.prepare(`SELECT id FROM live_read_cards WHERE owner_id=? AND scope='connect' AND operation_id=?
174
235
  AND json_extract(accepted_input_json,'$.action')='disconnect'`).get(this.ownerId, id);
175
- if (disconnect && disconnect.id !== source.cardId)
236
+ if (source.action === "connect" && disconnect && disconnect.id !== source.cardId)
237
+ return;
238
+ const activeDisconnect = this.pendingDisconnect(id);
239
+ if (source.action === "disconnect" && activeDisconnect && activeDisconnect.state.cardId !== source.cardId)
176
240
  return;
177
241
  const record = this.connection(id);
178
- if (!record || record.ownerId !== this.ownerId)
242
+ if (!record || record.ownerId !== this.ownerId || record.runId !== source.runId)
179
243
  return;
180
244
  if (source.action === "connect" && record.connection.status === "awaiting_approval") {
181
245
  const status = source.unrecordedOutcome ?? "failed";
@@ -239,12 +303,10 @@ export class SqliteWalletWorkflowStore {
239
303
  if (card.state.kind === "review" && !session)
240
304
  throw new WorkflowConflict("The saved review is unavailable.");
241
305
  const connection = card.state.kind === "connect" && card.operationId ? this.connection(card.operationId)?.connection : undefined;
242
- // Wallet-dependent commands already passed the workflow availability guard.
243
- // This store checks DB eligibility; it does not measure SDK health.
244
- const storedAccount = this.readActiveAccount(), activeAccount = storedAccount?.address, targetAccount = session?.account ?? activeAccount;
245
- const facts = { evaluatedAt: now.toISOString(), ownerId: this.ownerId, record: card, session, request,
246
- authority: request ? this.authority(request.attemptId) : undefined, connection, connections: this.connectionViews(),
247
- walletAvailability: { status: "available" }, activeAccount, activeAccountConnectionId: storedAccount?.walletId,
306
+ const connectionFacts = this.readConnectionFacts(now), targetAccount = session?.account ?? connectionFacts.activeAccount;
307
+ const facts = { ...connectionFacts, ownerId: this.ownerId, record: card, session, request,
308
+ authority: request ? this.authority(request.attemptId) : undefined, connection,
309
+ walletAvailability: { status: "available" },
248
310
  busyForAccount: !!targetAccount && this.busyForAccount(targetAccount, now) };
249
311
  if (!workflowEligibility(facts).allowedActions.includes(action))
250
312
  throw new WorkflowConflict("That action is not available right now.");
@@ -305,19 +367,51 @@ export class SqliteWalletWorkflowStore {
305
367
  WHERE operation_id=@id`).run({ terminal: terminal ? 1 : 0, id: operationId });
306
368
  }
307
369
  publishConnection(connectionId, status) {
308
- // Connection updates are not completion of every operation referencing it.
309
- // In particular, session_update(connected) must not finish a pending disconnect.
370
+ const outcome = this.connection(connectionId)?.connection;
310
371
  this.db.prepare(`UPDATE live_read_cards SET revision=revision+1,
372
+ result_json=CASE WHEN state='running' AND json_extract(accepted_input_json,'$.action')='disconnect'
373
+ AND @disconnectTerminal THEN @outcome ELSE result_json END,
311
374
  reason=CASE WHEN state='running' AND
312
375
  CASE WHEN json_extract(accepted_input_json,'$.action')='disconnect'
313
376
  THEN @disconnectTerminal ELSE @connectionTerminal END THEN 'completed' ELSE reason END,
314
377
  state=CASE WHEN state='running' AND
315
378
  CASE WHEN json_extract(accepted_input_json,'$.action')='disconnect'
316
379
  THEN @disconnectTerminal ELSE @connectionTerminal END THEN 'closed' ELSE state END
317
- WHERE scope='connect' AND operation_id=@id`).run({ id: connectionId,
380
+ WHERE scope='connect' AND operation_id=@id`).run({ id: connectionId, outcome: outcome ? JSON.stringify({ disconnectOutcome: outcome }) : null,
318
381
  disconnectTerminal: Number(status !== "connected" && status !== "awaiting_approval"),
319
382
  connectionTerminal: Number(status !== "awaiting_approval") });
320
383
  }
384
+ recoveryImpact(runId) {
385
+ return walletRecoveryImpactSchema.parse({
386
+ runId,
387
+ connections: this.connections().filter((record) => record.ownerId === this.ownerId && record.runId === runId)
388
+ .map((record) => ({ connectionId: record.connection.connectionId, revision: record.connection.revision,
389
+ status: record.connection.status, accounts: record.connection.accounts, walletName: record.connection.walletName,
390
+ sdkPending: record.sdkPending, disconnectPending: !!this.pendingDisconnect(record.connection.connectionId) })),
391
+ preparations: this.db.prepare(`SELECT id,revision,review_revision,preparation_id,wallet_connection_id FROM live_review_sessions
392
+ WHERE owner_id=? AND preparation_run_id=? AND status!='expired' ORDER BY id`).all(this.ownerId, runId),
393
+ requests: this.db.prepare(`SELECT r.attempt_id,r.revision,r.request_status,l.can_submit,l.sdk_pending,l.submit_pending,l.lookup_pending
394
+ FROM review_requests r JOIN live_request_authority l ON l.attempt_id=r.attempt_id
395
+ WHERE l.owner_id=? AND (l.run_id=? OR r.request_status IN ('submitting','awaiting_chain_result','outcome_unknown'))
396
+ ORDER BY r.attempt_id`).all(this.ownerId, runId)
397
+ });
398
+ }
399
+ recordRunExit(runId, parentProcessing = []) {
400
+ this.db.transaction(() => {
401
+ this.db.prepare(`UPDATE live_request_authority SET sdk_pending=0 WHERE owner_id=? AND run_id=? AND sdk_pending=1
402
+ AND attempt_id NOT IN (SELECT value FROM json_each(?))`).run(this.ownerId, runId, JSON.stringify(parentProcessing));
403
+ for (const record of this.connections()) {
404
+ if (record.ownerId !== this.ownerId || record.runId !== runId)
405
+ continue;
406
+ const pending = this.pendingDisconnect(record.connection.connectionId);
407
+ if (pending)
408
+ this.finishDisconnect(pending.state.cardId, record.connection.connectionId, false, this.clock());
409
+ else if (record.connection.status === "awaiting_approval")
410
+ this.updateConnection(record.connection.connectionId, { status: "stopped", reason: "The wallet service ended before this connection was confirmed." }, this.clock(), undefined, false);
411
+ this.db.prepare("UPDATE live_wallet_connections SET sdk_pending=0 WHERE id=? AND run_id=? AND sdk_pending=1").run(record.connection.connectionId, runId);
412
+ }
413
+ }).immediate();
414
+ }
321
415
  publishAvailableWallets() {
322
416
  this.db.prepare("UPDATE live_read_cards SET revision=revision+1 WHERE owner_id=? AND kind IN ('connect','review') AND state='ready'").run(this.ownerId);
323
417
  }
@@ -328,17 +422,16 @@ export class SqliteWalletWorkflowStore {
328
422
  admitConnection(expected, input) {
329
423
  return this.db.transaction(() => {
330
424
  const now = this.clock();
425
+ this.assertWalletInputAllowed();
331
426
  this.expireConnections(now);
332
427
  const card = this.requireInput(expected, now);
333
428
  this.requireAction(card, "connect", now);
334
429
  const connection = walletConnectionSchema.parse({ connectionId: randomUUID(), status: "awaiting_approval", revision: 0,
335
430
  accounts: [], methods: [], chain: SUI_MAINNET_WALLET_CHAIN, createdAt: now.toISOString(), updatedAt: now.toISOString(),
336
431
  expiresAt: card.state.expiresAt });
337
- this.db.prepare("INSERT INTO live_wallet_connections VALUES (?,?,?,?,?,?,?)")
338
- .run(connection.connectionId, this.ownerId, connection.status, 0, null, 1, JSON.stringify(connection));
432
+ this.db.prepare("INSERT INTO live_wallet_connections (id,owner_id,status,revision,topic,sdk_pending,connection_json,run_id) VALUES (?,?,?,?,?,?,?,?)")
433
+ .run(connection.connectionId, this.ownerId, connection.status, 0, null, 1, JSON.stringify(connection), this.runId ?? null);
339
434
  this.consume(card, input, connection.connectionId);
340
- // Every other live input observes the admitted connection before the SDK
341
- // responds, so an older selection cannot bypass the one-connection rule.
342
435
  this.publishAvailableWallets();
343
436
  return this.connection(connection.connectionId);
344
437
  }).immediate();
@@ -372,20 +465,24 @@ export class SqliteWalletWorkflowStore {
372
465
  const existing = this.db.prepare("SELECT id FROM live_wallet_connections WHERE topic=?").get(session.topic);
373
466
  const id = existing?.id ?? randomUUID();
374
467
  const prior = existing ? this.connection(id) : undefined;
468
+ if (prior?.ownerId === this.ownerId && prior.runId === this.runId && prior.connection.status === "connected" && !prior.sdkPending &&
469
+ isDeepStrictEqual(prior.connection.accounts, session.accounts) && isDeepStrictEqual(prior.connection.methods, session.methods) &&
470
+ prior.connection.expiresAt === session.expiresAt && prior.connection.walletName === session.walletName)
471
+ return prior;
375
472
  const next = walletConnectionSchema.parse({ connectionId: id, status: "connected", revision: (prior?.connection.revision ?? -1) + 1,
376
473
  accounts: session.accounts, methods: session.methods, chain: session.chain,
377
474
  createdAt: prior?.connection.createdAt ?? now.toISOString(), updatedAt: now.toISOString(),
378
475
  expiresAt: session.expiresAt, walletName: session.walletName });
379
- this.db.prepare(`INSERT INTO live_wallet_connections VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET
476
+ this.db.prepare(`INSERT INTO live_wallet_connections (id,owner_id,status,revision,topic,sdk_pending,connection_json,run_id) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET
380
477
  owner_id=excluded.owner_id, status=excluded.status, revision=excluded.revision, topic=excluded.topic,
381
- sdk_pending=0, connection_json=excluded.connection_json`).run(id, this.ownerId, next.status, next.revision, session.topic, 0, JSON.stringify(next));
478
+ sdk_pending=0, connection_json=excluded.connection_json,run_id=excluded.run_id`).run(id, this.ownerId, next.status, next.revision, session.topic, 0, JSON.stringify(next), this.runId ?? null);
382
479
  this.publishConnection(id, next.status);
383
- // Restoring the SDK session never restores a cleared read-account context.
384
480
  return this.connection(id);
385
481
  }
386
482
  useAccount(expected, input, connectionId, account) {
387
483
  this.db.transaction(() => {
388
484
  const now = this.clock();
485
+ this.assertWalletInputAllowed();
389
486
  const card = this.requireInput(expected, now), connection = this.connection(connectionId);
390
487
  if (card.scope !== "connect" || !this.isUsableConnection(connectionId, now) || !isOwnedConnectedWallet(connection, this.ownerId, !!this.pendingDisconnect(connectionId)) ||
391
488
  Date.parse(connection.connection.expiresAt) <= now.getTime() || !connection.connection.accounts.includes(account)) {
@@ -400,8 +497,10 @@ export class SqliteWalletWorkflowStore {
400
497
  admitDisconnect(expected, input, connectionId) {
401
498
  return this.db.transaction(() => {
402
499
  const now = this.clock();
500
+ this.assertWalletInputAllowed();
403
501
  const card = this.requireInput(expected, now), connection = this.connection(connectionId);
404
- if (card.scope !== "connect" || !isOwnedConnectedWallet(connection, this.ownerId, !!this.pendingDisconnect(connectionId))) {
502
+ if (card.scope !== "connect" || !connection || connection.ownerId !== this.ownerId || connection.runId !== this.runId ||
503
+ connection.sdkPending || !this.connectionSelection(now).manageableConnectionIds.includes(connectionId)) {
405
504
  throw new WorkflowConflict("The wallet connection is no longer available.");
406
505
  }
407
506
  this.requireAction(card, "disconnect", now);
@@ -415,7 +514,6 @@ export class SqliteWalletWorkflowStore {
415
514
  finishDisconnect(cardId, connectionId, succeeded, now) {
416
515
  this.db.transaction(() => {
417
516
  const pending = this.pendingDisconnect(connectionId);
418
- // A session deletion event may have already completed this operation.
419
517
  if (pending?.state.cardId !== cardId)
420
518
  return;
421
519
  this.updateConnection(connectionId, succeeded ? { status: "disconnected" } : {
@@ -456,6 +554,7 @@ export class SqliteWalletWorkflowStore {
456
554
  admitRequest(expected, input, material, selectedConnection, walletRunId) {
457
555
  const result = this.db.transaction(() => {
458
556
  const now = this.clock();
557
+ this.assertWalletInputAllowed();
459
558
  const binding = { reviewSessionId: material.reviewSessionId, planId: material.planId, account: material.account,
460
559
  reviewRevision: material.reviewRevision, transactionDigest: material.reviewedTransactionDigest,
461
560
  connectionId: selectedConnection.connectionId, connectionRevision: selectedConnection.revision };
@@ -469,9 +568,8 @@ export class SqliteWalletWorkflowStore {
469
568
  const artifacts = new SqlitePrivateReviewArtifactStore(this.db).get(material.reviewSessionId);
470
569
  const stored = this.materialStore.getTransactionMaterial(material.transactionMaterial, now);
471
570
  if (!current || !materialStillMatches(material, current, sessions.revision(current.id), artifacts, stored, now)) {
472
- // Expected expiry commits its normal cleanup, but no request authority.
473
- // A changed candidate is a conflict and must not invalidate newer data.
474
- const evaluated = this.evaluateState({ expectedCard: card, candidate: { session: material.review, rowRevision: material.rowRevision,
571
+ const evaluated = this.evaluateState({ purpose: "review", reviewSessionId: material.reviewSessionId,
572
+ readTarget: this.reviewReadTarget(material.reviewSessionId, card), expectedCard: card, candidate: { session: material.review, rowRevision: material.rowRevision,
475
573
  artifacts: material.artifacts, material }, walletAvailability: { status: "available", walletRunId } }, now);
476
574
  return new WorkflowConflict("The transaction details used by this review are no longer current.", evaluated.events);
477
575
  }
@@ -498,8 +596,8 @@ export class SqliteWalletWorkflowStore {
498
596
  (attempt_id,review_session_id,plan_id,review_revision,account_id,transaction_digest,review_state_json,request_status,revision,created_at,updated_at)
499
597
  VALUES (?,?,?,?,?,?,?,'awaiting_signature',0,?,?)`).run(id, review.id, binding.planId, binding.reviewRevision, account.id, binding.transactionDigest, review.review_state_json, now.toISOString(), now.toISOString());
500
598
  this.db.prepare(`INSERT INTO live_request_authority
501
- (attempt_id,owner_id,connection_id,connection_revision,can_submit,sdk_pending,submit_pending,lookup_pending,signature_deadline)
502
- VALUES (?,?,?,?,1,1,0,0,?)`).run(id, this.ownerId, binding.connectionId, binding.connectionRevision, new Date(Math.min(now.getTime() + SIGNATURE_WAIT_MS, Date.parse(review.expires_at))).toISOString());
599
+ (attempt_id,owner_id,connection_id,connection_revision,can_submit,sdk_pending,submit_pending,lookup_pending,signature_deadline,run_id)
600
+ VALUES (?,?,?,?,1,1,0,0,?,?)`).run(id, this.ownerId, binding.connectionId, binding.connectionRevision, new Date(Math.min(now.getTime() + SIGNATURE_WAIT_MS, Date.parse(review.expires_at))).toISOString(), walletRunId);
503
601
  this.db.prepare(`UPDATE live_review_sessions SET current_attempt_id=?, revision=revision+1, write_contract_version=? WHERE id=?`)
504
602
  .run(id, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION, review.id);
505
603
  this.db.prepare("UPDATE review_sessions SET current_attempt_id=?, updated_at=? WHERE id=?").run(id, now.toISOString(), review.id);
@@ -590,8 +688,6 @@ export class SqliteWalletWorkflowStore {
590
688
  if (old.requestStatus === "awaiting_signature" && next !== old.requestStatus)
591
689
  this.publishAvailableWallets();
592
690
  if (["stopped", "request_failed", "outcome_unknown", "completed"].includes(next)) {
593
- // A historical attempt may finish while a newer review is preparing.
594
- // Clean only the material still owned by this admitted revision.
595
691
  for (const table of ["live_transaction_materials", "live_private_review_artifacts"]) {
596
692
  this.db.prepare(`DELETE FROM ${table} WHERE review_session_id=? AND EXISTS
597
693
  (SELECT 1 FROM live_review_sessions WHERE id=? AND review_revision=? AND preparation_id IS NULL)`)
@@ -605,7 +701,8 @@ export class SqliteWalletWorkflowStore {
605
701
  return this.db.transaction(() => {
606
702
  const now = this.clock();
607
703
  const request = this.request(id), authority = this.authority(id);
608
- if (!request || !authority || authority.owner_id !== this.ownerId || authority.can_submit !== 1 ||
704
+ if (!request || !authority || authority.owner_id !== this.ownerId || authority.can_submit !== 1 || authority.run_id !== (this.runId ?? null) ||
705
+ this.db.prepare("SELECT wallet_input_blocked FROM settings_control WHERE id=1").get().wallet_input_blocked !== 0 ||
609
706
  request.requestStatus !== "awaiting_signature" || Date.parse(authority.signature_deadline) <= now.getTime())
610
707
  return false;
611
708
  const connection = this.connection(authority.connection_id);
@@ -621,8 +718,6 @@ export class SqliteWalletWorkflowStore {
621
718
  this.db.transaction(() => {
622
719
  const now = this.clock();
623
720
  this.advanceRequestDeadlines(now, id);
624
- // Observation may have finished, or its window may have closed, while
625
- // submit was pending. A late response cannot reopen either outcome.
626
721
  if (this.request(id)?.requestStatus === "submitting")
627
722
  this.transitionRequest(id, "awaiting_chain_result", now);
628
723
  }).immediate();
@@ -637,8 +732,6 @@ export class SqliteWalletWorkflowStore {
637
732
  return request;
638
733
  if (request.requestStatus === "awaiting_signature")
639
734
  return this.transitionRequest(id, cause === "wallet_service_lost" ? "request_failed" : "stopped", now, { reason: interruptionReasons[cause] });
640
- // Once submitted, connection changes cannot revoke a transaction or the
641
- // independent lookup of its digest. Only explicit user stopping ends observation.
642
735
  if (cause === "user_stop" && authority.observation_stopped === 0 &&
643
736
  ["submitting", "awaiting_chain_result", "outcome_unknown"].includes(request.requestStatus)) {
644
737
  this.db.prepare("UPDATE live_request_authority SET observation_stopped=1 WHERE attempt_id=?").run(id);
@@ -650,15 +743,14 @@ export class SqliteWalletWorkflowStore {
650
743
  resumeObservation(id) {
651
744
  this.db.prepare("UPDATE live_request_authority SET observation_stopped=0 WHERE attempt_id=? AND owner_id=?").run(id, this.ownerId);
652
745
  }
653
- invalidateConnectionRequests(connectionId, now, cause) {
746
+ invalidateConnectionRequests(connectionId, now, cause, runId) {
654
747
  this.db.transaction(() => {
655
748
  const rows = this.db.prepare(`SELECT * FROM live_review_sessions v WHERE wallet_connection_id=? AND owner_id=? AND status!='expired'
749
+ AND (? IS NULL OR v.preparation_run_id=?)
656
750
  AND NOT EXISTS (SELECT 1 FROM review_requests r WHERE r.review_session_id=v.id AND r.review_revision=v.review_revision)`)
657
- .all(connectionId, this.ownerId);
751
+ .all(connectionId, this.ownerId, runId ?? null, runId ?? null);
658
752
  for (const row of rows) {
659
753
  const state = row.review_state_json ? JSON.parse(row.review_state_json) : undefined;
660
- // Later session observations cannot rewrite why an already interrupted
661
- // preparation ended. A new admitted preparation clears this error.
662
754
  if (!row.preparation_id && row.preparation_error && !state?.transactionReviewData)
663
755
  continue;
664
756
  if (state) {
@@ -676,8 +768,8 @@ export class SqliteWalletWorkflowStore {
676
768
  this.publishReview(row.id);
677
769
  }
678
770
  }).immediate();
679
- for (const row of this.db.prepare("SELECT attempt_id FROM live_request_authority WHERE connection_id=? AND owner_id=?")
680
- .all(connectionId, this.ownerId))
771
+ for (const row of this.db.prepare("SELECT attempt_id FROM live_request_authority WHERE connection_id=? AND owner_id=? AND (? IS NULL OR run_id=?)")
772
+ .all(connectionId, this.ownerId, runId ?? null, runId ?? null))
681
773
  this.stopWaiting(row.attempt_id, now, cause);
682
774
  }
683
775
  cancelInput(expected) {
@@ -693,6 +785,7 @@ export class SqliteWalletWorkflowStore {
693
785
  beginReviewPreparation(expected, reviewRevision, connection, account) {
694
786
  return this.db.transaction(() => {
695
787
  const now = this.clock();
788
+ this.assertWalletInputAllowed();
696
789
  const card = this.requireInput(expected, now);
697
790
  if (card.scope !== "review" || typeof card.state.input.reviewSessionId !== "string")
698
791
  throw new WorkflowConflict("This card cannot update the review.");
@@ -713,8 +806,8 @@ export class SqliteWalletWorkflowStore {
713
806
  if (this.busyForAccount(selection.account, now) || !currentConnection.connection.accounts.includes(selection.account))
714
807
  throw new WorkflowConflict("The review could not be updated with the selected wallet account.");
715
808
  const preparation = randomUUID();
716
- this.db.prepare(`UPDATE live_review_sessions SET preparation_id=?,preparation_error=NULL,wallet_connection_id=?,wallet_connection_revision=?,revision=revision+1,write_contract_version=? WHERE id=?`)
717
- .run(preparation, connection.connectionId, connection.revision, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION, id);
809
+ this.db.prepare(`UPDATE live_review_sessions SET preparation_id=?,preparation_run_id=?,preparation_error=NULL,wallet_connection_id=?,wallet_connection_revision=?,revision=revision+1,write_contract_version=? WHERE id=?`)
810
+ .run(preparation, this.runId ?? null, connection.connectionId, connection.revision, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION, id);
718
811
  this.publishReview(id);
719
812
  return preparation;
720
813
  }).immediate();
@@ -798,10 +891,11 @@ export class SqliteWalletWorkflowStore {
798
891
  }
799
892
  }
800
893
  export function hasUnsettledWalletWork(db, now = new Date()) {
801
- return !!db.prepare(`SELECT 1 FROM live_request_authority l JOIN review_requests r ON r.attempt_id=l.attempt_id
894
+ return !!db.prepare("SELECT 1 FROM live_wallet_connections WHERE sdk_pending=1 LIMIT 1").get() ||
895
+ !!db.prepare(`SELECT 1 FROM live_request_authority l JOIN review_requests r ON r.attempt_id=l.attempt_id
802
896
  WHERE l.sdk_pending=1 OR l.submit_pending=1 OR l.lookup_pending=1
803
897
  OR (r.request_status IN (SELECT value FROM json_each(?)) AND (l.lookup_deadline IS NULL OR l.lookup_deadline>?)) LIMIT 1`)
804
- .get(JSON.stringify(INITIAL_CHAIN_OBSERVATION_STATUSES), now.toISOString());
898
+ .get(JSON.stringify(INITIAL_CHAIN_OBSERVATION_STATUSES), now.toISOString());
805
899
  }
806
900
  export function readStoredTransactionRequest(db, id) {
807
901
  const row = db.prepare(`SELECT r.*, a.sui_address AS account, e.result_json FROM review_requests r
@@ -32,8 +32,6 @@ export function isInteractionPendingReviewStatus(status) {
32
32
  return executionStatusCategory(status) !== "final";
33
33
  }
34
34
  export function isReviewInteractionPending({ hasReviewInput, request, progress }) {
35
- // Historical preparation state is not proof that a live input remains.
36
- // Consumed input, in turn, does not end its admitted request or observation.
37
35
  return hasReviewInput || progress.status !== "idle" || !!request && isRequestWaiting(request.requestStatus);
38
36
  }
39
37
  export function executionPollingHint() {
@@ -46,10 +44,10 @@ export function executionPollingHint() {
46
44
  recommendedIntervalSeconds: EXECUTION_POLLING_INTERVAL_SECONDS
47
45
  };
48
46
  }
49
- export function reviewStatusResponse({ session, request, walletAvailability, walletObservation, progress, connectionConflict }) {
47
+ export function reviewStatusResponse({ session, request, walletAvailability, walletObservation, progress, connectionConflict, connectionReconciliationRequired }) {
50
48
  const pollingStatus = getExecutionPollingStatus(session, request);
51
49
  return {
52
- walletAvailability, walletObservation, progress, connectionConflict,
50
+ walletAvailability, walletObservation, progress, connectionConflict, connectionReconciliationRequired,
53
51
  reviewSessionId: session.id, status: session.status, reviewRevision: session.reviewRevision,
54
52
  account: session.account, plans: session.plans, reviewState: session.reviewState,
55
53
  pollingStatus, statusCategory: executionStatusCategory(pollingStatus), pollingHint: executionPollingHint(),
@@ -115,9 +115,6 @@ export class LocalTransactionMaterialStoreError extends Error {
115
115
  super(message);
116
116
  }
117
117
  }
118
- // Validate the store input and build the stored record (with cloned bytes). Shared
119
- // by every LocalTransactionMaterialStore implementation so validation never drifts
120
- // between the in-memory and SQLite backends.
121
118
  export function buildTransactionMaterialRecord(input, now = new Date()) {
122
119
  const account = parseSuiAddress(input.account);
123
120
  if (!account) {
@@ -158,7 +155,6 @@ export function buildTransactionMaterialRecord(input, now = new Date()) {
158
155
  : { redactedDiagnostics: structuredClone(input.redactedDiagnostics) })
159
156
  };
160
157
  }
161
- // Project the public (redacted) handle out of a stored record.
162
158
  export function toMaterialHandle(record) {
163
159
  return {
164
160
  materialId: record.materialId,
@@ -6,14 +6,10 @@ export const TRANSACTION_REQUEST_STATUSES = [
6
6
  "request_failed", "outcome_unknown", "completed"
7
7
  ];
8
8
  export const transactionRequestStatusSchema = z.enum(TRANSACTION_REQUEST_STATUSES);
9
- // The submission was admitted durably before external I/O. A failed follow-up
10
- // write must not remove its initial observation window or replacement guard.
11
9
  export const INITIAL_CHAIN_OBSERVATION_STATUSES = ["submitting", "awaiting_chain_result"];
12
10
  export function isInitialChainObservation(status) {
13
11
  return INITIAL_CHAIN_OBSERVATION_STATUSES.some((value) => value === status);
14
12
  }
15
- // This is the existing signing surface's local wait, independent of protocol
16
- // request expiry, card input expiry and the chain observation window.
17
13
  export const SIGNATURE_WAIT_MS = 90_000;
18
14
  export const CHAIN_RECEIPT_LOOKUP_MAX_AGE_MS = 10 * 60 * 1000;
19
15
  export const transactionExecutionSummarySchema = z.object({
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { suiAddressStringSchema } from "../suiAddress.js";
3
+ import { transactionRequestStatusSchema } from "./transactionRequest.js";
3
4
  export const SUI_MAINNET_WALLET_CHAIN = "sui:mainnet";
4
5
  export const SUI_SIGN_TRANSACTION_METHOD = "sui_signTransaction";
5
6
  export const WALLET_CONNECTION_POLL_SECONDS = 5;
@@ -45,6 +46,17 @@ export function workflowProgress(waiting, requiresWallet, availability) {
45
46
  export const walletConnectionStatusSchema = z.enum([
46
47
  "awaiting_approval", "connected", "rejected", "failed", "expired", "stopped", "disconnected"
47
48
  ]);
49
+ export const walletRecoveryImpactSchema = z.object({
50
+ runId: z.string().uuid(),
51
+ connections: z.array(z.object({ connectionId: z.string().min(1), revision: z.number().int().nonnegative(),
52
+ status: walletConnectionStatusSchema, accounts: z.array(suiAddressStringSchema), walletName: z.string().optional(),
53
+ sdkPending: z.boolean(), disconnectPending: z.boolean() }).strict()),
54
+ preparations: z.array(z.object({ id: z.string().min(1), revision: z.number().int().nonnegative(),
55
+ review_revision: z.number().int().nonnegative(), preparation_id: z.string().nullable(), wallet_connection_id: z.string().nullable() }).strict()),
56
+ requests: z.array(z.object({ attempt_id: z.string().min(1), revision: z.number().int().nonnegative(),
57
+ request_status: transactionRequestStatusSchema, can_submit: z.number().int().min(0).max(1),
58
+ sdk_pending: z.number().int().min(0).max(1), submit_pending: z.number().int().min(0).max(1), lookup_pending: z.number().int().min(0).max(1) }).strict())
59
+ }).strict();
48
60
  export const walletConnectionSchema = z.object({
49
61
  connectionId: z.string().min(1),
50
62
  status: walletConnectionStatusSchema,
@@ -63,21 +75,27 @@ export const connectionConflictSchema = z.object({
63
75
  connectionIds: z.array(z.string().min(1)).min(2).refine((ids) => new Set(ids).size === ids.length)
64
76
  }).strict();
65
77
  export const CONNECTION_CONFLICT_MESSAGE = "More than one wallet connection is saved. Disconnect the connections you no longer need before continuing.";
78
+ export const CONNECTION_RECONCILIATION_MESSAGE = "A wallet connection in the connection service is not recorded here. Remove the retained connection in your wallet app, then check its status. If needed, open Settings and use Troubleshooting to restart the connection service and inspect it again. Restarting does not approve an unrecorded connection.";
66
79
  export const assetReadAccountSchema = z.discriminatedUnion("status", [
67
80
  z.object({ status: z.literal("available"), account: suiAddressStringSchema }).strict(),
68
81
  z.object({ status: z.literal("address_required") }).strict()
69
82
  ]);
70
- // Management may name every recorded target. Account use requires exactly one
71
- // live connection and no connection operation still awaiting its outcome.
72
- export function walletConnectionSelection(connections, at, stored) {
83
+ export function walletConnectionSelection(connections, at, presence, stored) {
73
84
  const connected = connections.filter((item) => item.status === "connected" && Date.parse(item.expiresAt) > at);
74
- const pending = connections.some((item) => item.pendingAction || item.status === "awaiting_approval");
75
- const connection = connected.length === 1 && !pending ? connected[0] : undefined;
85
+ const pending = presence.pendingConnectionIds.length > 0 || connections.some((item) => item.pendingAction || item.status === "awaiting_approval");
86
+ const presentIds = presence.connections.map((item) => item.connectionId);
87
+ const occupied = new Set([...connected.map((item) => item.connectionId), ...presentIds]);
88
+ const reconciliation = presence.confirmed && presence.unrecorded && !pending;
89
+ const connection = presence.confirmed && !presence.unrecorded && occupied.size === 1 && connected.length === 1 &&
90
+ presence.connections.some((item) => item.connectionId === connected[0].connectionId && Date.parse(item.expiresAt) > at) && !pending ? connected[0] : undefined;
76
91
  return {
77
- ...(connected.length > 1 ? { conflict: { reason: "multiple_connections",
78
- connectionIds: connected.map((item) => item.connectionId).sort() } } : {}),
92
+ ...(occupied.size > 1 ? { conflict: { reason: "multiple_connections",
93
+ connectionIds: [...occupied].sort() } } : {}),
94
+ ...(reconciliation ? { connectionReconciliationRequired: true } : {}),
95
+ manageableConnectionIds: connections.filter((item) => !presence.pendingConnectionIds.includes(item.connectionId) && !item.pendingAction && item.status !== "awaiting_approval" &&
96
+ (item.status === "connected" || presence.confirmed && presentIds.includes(item.connectionId))).map((item) => item.connectionId),
79
97
  ...(connection ? { connection } : {}),
80
- pairingAllowed: connected.length === 0 && !pending,
98
+ pairingAllowed: presence.confirmed && !presence.unrecorded && occupied.size === 0 && !pending,
81
99
  assetReadAccount: connection && stored?.walletId === connection.connectionId && connection.accounts.includes(stored.address)
82
100
  ? { status: "available", account: stored.address } : { status: "address_required" }
83
101
  };
@@ -85,8 +103,6 @@ export function walletConnectionSelection(connections, at, stored) {
85
103
  export class WalletUserRejectedError extends Error {
86
104
  constructor() { super("The wallet request was rejected by the user."); }
87
105
  }
88
- // Shared recorded connection facts only. Each operation checks its own expiry,
89
- // account, method and revision requirements at its decision boundary.
90
106
  export function isOwnedConnectedWallet(record, ownerId, disconnectPending) {
91
107
  return !!record && record.ownerId === ownerId && record.connection.status === "connected" && !disconnectPending;
92
108
  }