@zktx.io/sui-mcp 0.7.0 → 0.8.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 (71) hide show
  1. package/README.md +89 -42
  2. package/dist/core/activity/localDataService.js +8 -21
  3. package/dist/core/activity/localDataValidation.js +22 -46
  4. package/dist/core/activity/sqliteAccountStore.js +46 -0
  5. package/dist/core/activity/sqliteActivityStore.js +65 -147
  6. package/dist/core/activity/sqliteActivityStoreSchema.js +44 -28
  7. package/dist/core/activity/sqliteActivityStoreTypes.js +9 -0
  8. package/dist/core/activity/transactionActivityService.js +2 -2
  9. package/dist/core/preferences/preferencesStore.js +2 -2
  10. package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
  11. package/dist/core/session/cardResult.js +41 -0
  12. package/dist/core/session/cardSession.js +1 -1
  13. package/dist/core/session/cardSessionStore.js +51 -19
  14. package/dist/core/session/chainAccounts.js +120 -0
  15. package/dist/core/session/sessionStore.js +4 -8
  16. package/dist/core/session/settingsFileStore.js +3 -30
  17. package/dist/core/session/settingsOperation.js +27 -19
  18. package/dist/core/session/settingsWorkflow.js +161 -87
  19. package/dist/core/session/sqliteCardStore.js +34 -14
  20. package/dist/core/session/sqliteSessionStore.js +5 -5
  21. package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
  22. package/dist/core/session/sqliteTransactionRequest.js +18 -0
  23. package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
  24. package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
  25. package/dist/core/session/status.js +2 -2
  26. package/dist/core/session/wait.js +6 -11
  27. package/dist/core/session/walletConnection.js +71 -46
  28. package/dist/core/session/walletConnectionOperations.js +24 -0
  29. package/dist/core/session/walletRuntime.js +7 -13
  30. package/dist/core/session/walletWorkflow.js +248 -99
  31. package/dist/core/session/workflowState.js +104 -38
  32. package/dist/core/session/workflowView.js +92 -101
  33. package/dist/mcp/activeAccountResponse.js +3 -0
  34. package/dist/mcp/responseGuidance.js +7 -9
  35. package/dist/mcp/serverInfo.js +11 -10
  36. package/dist/mcp/toolErrors.js +5 -3
  37. package/dist/mcp/toolNames.js +1 -0
  38. package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
  39. package/dist/mcp/tools/read/readToolHelpers.js +9 -7
  40. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  41. package/dist/mcp/tools/session/shared.js +2 -2
  42. package/dist/mcp/tools/session/statusTools.js +12 -10
  43. package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
  44. package/dist/mcp/tools/settings/index.js +7 -4
  45. package/dist/mcp-app/account.html +7 -7
  46. package/dist/mcp-app/chart.html +3 -3
  47. package/dist/mcp-app/connect.html +13 -13
  48. package/dist/mcp-app/disconnect.html +3637 -0
  49. package/dist/mcp-app/disconnect.notices.txt +1480 -0
  50. package/dist/mcp-app/receipt.html +393 -393
  51. package/dist/mcp-app/review.html +459 -459
  52. package/dist/mcp-app/settings.html +7 -7
  53. package/dist/mcp-ui/contracts.js +3 -1
  54. package/dist/mcp-ui/tools.js +4 -4
  55. package/dist/runtime/application.js +1 -1
  56. package/dist/runtime/localSettingsService.js +7 -4
  57. package/dist/runtime/runtimeTermination.js +3 -0
  58. package/dist/runtime/smokeMainnetRead.js +5 -4
  59. package/dist/runtime/walletConnectTransport.js +16 -16
  60. package/dist/runtime/walletSdkChildRuntime.js +29 -12
  61. package/dist/runtime/walletSdkIpc.js +5 -4
  62. package/dist/runtime/walletSdkProcess.js +24 -8
  63. package/docs/AGENT_BEHAVIOR.md +82 -54
  64. package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
  65. package/docs/MCP_SETUP.md +384 -347
  66. package/docs/MCP_TOOLS.md +292 -177
  67. package/docs/SDK_API.md +15 -6
  68. package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
  69. package/docs/UTILITY_INDEX.md +9 -7
  70. package/docs/WALLET_CONNECTION.md +162 -167
  71. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { workflowConnectionEligibility } from "./workflowState.js";
1
+ import { randomUUID } from "node:crypto";
2
+ import { fixedReviewTarget, workflowConnectionEligibility, walletActionDecision } from "./workflowState.js";
2
3
  import { SessionStoreError } from "./sessionErrors.js";
3
4
  import { Transaction } from "@mysten/sui/transactions";
4
5
  import { verifyTransactionSignature } from "@mysten/sui/verify";
@@ -6,15 +7,18 @@ import { computeReviewStateWithPrivateArtifacts } from "../review/reviewComputat
6
7
  import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
7
8
  import { projectReadCardResult } from "../read/readCardResult.js";
8
9
  import { WorkflowConflict } from "./sqliteWalletWorkflowStore.js";
9
- import { isOwnedConnectedWallet, SUI_SIGN_TRANSACTION_METHOD, WalletUserRejectedError, WalletUnavailableError } from "./walletConnection.js";
10
+ import { isOwnedConnectedWallet, connectionHasSuiSigningScope, WalletUserRejectedError, WalletUnavailableError } from "./walletConnection.js";
10
11
  import { isInitialChainObservation } from "./transactionRequest.js";
11
12
  import { projectConnectionView, projectReviewView } from "./workflowView.js";
12
13
  import { WalletRunInterruptedError } from "./walletRuntime.js";
14
+ import { SUI_MAINNET_WALLET_CHAIN, walletGrantAccount, fixedChainAccount } from "./chainAccounts.js";
15
+ import { walletConnectionCardInputSchema, walletConnectionActionSchema } from "./walletConnectionOperations.js";
13
16
  import { reviewProgress } from "./status.js";
14
17
  export class WalletWorkflow {
15
18
  options;
16
19
  stopped = false;
17
20
  started = false;
21
+ retainedSessionCheck;
18
22
  pairings = new Map();
19
23
  observing = new Map();
20
24
  preparing = new Set();
@@ -40,7 +44,7 @@ export class WalletWorkflow {
40
44
  run?.assertCurrent();
41
45
  const availability = this.walletAvailability();
42
46
  if (availability.status !== "available")
43
- throw new WalletUnavailableError(availability.status === "unavailable" ? availability.reason : "wallet_state_unavailable", availability.message);
47
+ throw new WalletUnavailableError(availability);
44
48
  }
45
49
  report(stage) { try {
46
50
  this.options.logger.error("Wallet operation failed", { stage });
@@ -69,10 +73,10 @@ export class WalletWorkflow {
69
73
  if (event.ready) {
70
74
  runtime.publishReady(event.runId);
71
75
  this.settingsRecovery?.readyPublished(event.runId);
76
+ void this.inspectRetainedSession().catch(() => this.report("retained_session_check"));
72
77
  }
73
78
  }
74
79
  else if (event.type === "stage") {
75
- this.options.records.publishWalletState();
76
80
  }
77
81
  else if (event.type === "lost") {
78
82
  this.pairings.clear();
@@ -135,7 +139,7 @@ export class WalletWorkflow {
135
139
  return;
136
140
  }
137
141
  const key = completion.kind === "runtime_failure" || completion.kind === "runtime_exit" ? `${completion.kind}:${completion.runId}` :
138
- completion.kind === "request" ? `request:${completion.id}:${completion.field}` : `connection:${completion.cardId}:${completion.runId}`;
142
+ completion.kind === "request" ? `request:${completion.id}:${completion.field}` : `${completion.kind}:${completion.operationId}:${completion.runId}`;
139
143
  if (!this.pendingWrites.has(key))
140
144
  this.pendingWrites.set(key, completion);
141
145
  try {
@@ -146,6 +150,31 @@ export class WalletWorkflow {
146
150
  }
147
151
  }
148
152
  settlePendingWrite(key, completion) {
153
+ if (completion.kind === "connection_cleanup") {
154
+ if (completion.runId !== this.options.runtime.runId) {
155
+ this.pendingWrites.delete(key);
156
+ return;
157
+ }
158
+ const captured = this.options.records.captureConnectionSession(completion, completion.session);
159
+ if (captured === "captured" && this.options.records.prepareConnectionCleanup(completion, completion.session, this.now())) {
160
+ this.pendingWrites.delete(key);
161
+ if (this.walletAvailability().status === "available") {
162
+ const run = this.options.runtime.bind();
163
+ void this.cleanupConnection(completion, run).catch(() => this.report("connection_cleanup"));
164
+ }
165
+ else {
166
+ this.completeCallback({ kind: "connection", id: completion.id, cardId: completion.cardId,
167
+ operationId: completion.operationId, runId: completion.runId, action: "connect",
168
+ cleanup: { topic: completion.session.topic, absent: false } });
169
+ }
170
+ }
171
+ else {
172
+ this.pendingWrites.delete(key);
173
+ this.completeCallback({ kind: "connection", id: completion.id, cardId: completion.cardId,
174
+ operationId: completion.operationId, runId: completion.runId, action: "connect" });
175
+ }
176
+ return;
177
+ }
149
178
  if (completion.kind === "request")
150
179
  this.options.records.settle(completion.id, completion.field, false);
151
180
  else if (completion.kind === "connection")
@@ -177,9 +206,13 @@ export class WalletWorkflow {
177
206
  if (walletDependent)
178
207
  this.options.runtime.synchronize();
179
208
  }
209
+ terminalCard(record) {
210
+ return { ...record, state: { ...record.state, data: this.options.records.cardResult(record, this.now()) } };
211
+ }
212
+ expireCard(record) { return this.options.records.expireCard(record, this.now()); }
180
213
  async prepare(kind, input) {
181
214
  this.assertCurrent();
182
- if (kind === "connect")
215
+ if (kind === "connect" || kind === "disconnect")
183
216
  return { status: "ready" };
184
217
  if (typeof input.reviewSessionId !== "string")
185
218
  throw new WorkflowConflict("The requested review was not specified.");
@@ -194,36 +227,126 @@ export class WalletWorkflow {
194
227
  if (!request || request.reviewSessionId !== session.id)
195
228
  throw new WorkflowConflict("This transaction request does not belong to the specified review.");
196
229
  }
197
- return { status: "ready" };
230
+ else if (!session.plans[0]?.reviewModel) {
231
+ const target = fixedReviewTarget(session, input);
232
+ const connection = target && this.options.records.connection(target.connectionId);
233
+ if (!target || !connection || fixedChainAccount(connection.connection, SUI_MAINNET_WALLET_CHAIN) !== target.account ||
234
+ !session.walletConnectionId && !connection.current) {
235
+ throw new WorkflowConflict("A review requires its fixed wallet connection and account. Start a new review from the current connection.");
236
+ }
237
+ }
238
+ return session.plans[0]?.reviewModel || input.mode === "manage" && ["completed", "stopped", "request_failed"].includes(this.options.records.request(String(input.attemptId))?.requestStatus ?? "") ? { status: "completed" } : { status: "ready" };
239
+ }
240
+ async connectionCardInput(kind, input) {
241
+ walletConnectionCardInputSchema.parse(input);
242
+ await this.inspectRetainedSession();
243
+ const context = this.reconcileConnectionContext(), facts = this.options.records.readConnectionFacts(this.now());
244
+ if (kind === "disconnect") {
245
+ this.assertWalletAvailable();
246
+ const decision = walletActionDecision({ ...facts, walletAvailability: context.walletAvailability }, "disconnect");
247
+ if (!decision.allowed)
248
+ throw new WorkflowConflict(decision.nextStep.message, [], decision.nextStep.reason);
249
+ return { connectionId: context.connection.connectionId, operationId: randomUUID() };
250
+ }
251
+ return { requestedChains: facts.requestedChains, connectionId: facts.connection?.connectionId ?? null, operationId: randomUUID() };
252
+ }
253
+ readOperation(record) {
254
+ const id = record.operationId;
255
+ if (id) {
256
+ for (const [key, write] of this.pendingWrites)
257
+ if ((write.kind === "connection" || write.kind === "connection_cleanup") && write.operationId === id) {
258
+ try {
259
+ this.settlePendingWrite(key, write);
260
+ this.pendingWrites.delete(key);
261
+ }
262
+ catch {
263
+ this.report("callback_record");
264
+ }
265
+ }
266
+ this.recoverDisconnects(id);
267
+ this.options.records.expireConnections(this.now(), id);
268
+ }
269
+ const walletAvailability = this.walletAvailability();
270
+ const result = this.options.records.operationRead(record, { walletAvailability, walletObservation: this.observation() });
271
+ return { ...result, ...(result.record.state.state !== "closed" && result.evaluated ? { data: projectConnectionView(result.evaluated) } : {}), walletAvailability };
272
+ }
273
+ closedReceiptDisplay(record) {
274
+ const data = record.state.data;
275
+ const request = data?.request;
276
+ if (!request?.execution)
277
+ return undefined;
278
+ const receiptDisplay = this.options.records.executionDetails(request.attemptId)?.receiptDisplay;
279
+ return receiptDisplay ? { receiptDisplay, displayAttemptId: request.attemptId } : undefined;
280
+ }
281
+ closeConnectionCardsWithChangedNetworks(chains) {
282
+ this.assertCurrent();
283
+ this.options.records.closeConnectionCardsWithChangedNetworks(chains);
284
+ }
285
+ resolveReviewTarget() {
286
+ const context = this.reconcileConnectionContext(), record = this.options.records.currentConnection();
287
+ if (context.walletAvailability.status !== "available")
288
+ return { status: "unavailable", reason: "wallet_unavailable" };
289
+ if (context.connectionReconciliationRequired)
290
+ return { status: "unavailable", reason: "connection_state_unconfirmed" };
291
+ const account = record && fixedChainAccount(record.connection, SUI_MAINNET_WALLET_CHAIN);
292
+ return account && record && this.options.records.isUsableConnection(record.connection.connectionId, this.now(), SUI_MAINNET_WALLET_CHAIN, account)
293
+ ? { status: "available", connectionId: record.connection.connectionId, account } : { status: "unavailable", reason: "connection_required" };
198
294
  }
199
295
  reconcileConnectionContext() {
200
296
  this.assertCurrent();
201
297
  this.synchronizeWalletState();
202
298
  this.recoverDisconnects();
203
299
  const state = this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
204
- return { connections: state.connections, connectionConflict: state.connectionConflict, walletAvailability: state.walletAvailability,
205
- walletObservation: state.walletObservation, manageableConnectionIds: state.manageableConnectionIds,
300
+ return { connection: state.connection, pendingOperation: state.pendingOperation, requestedChains: state.requestedChains, walletAvailability: state.walletAvailability,
301
+ walletObservation: state.walletObservation, canDisconnect: state.canDisconnect,
206
302
  connectionReconciliationRequired: state.connectionReconciliationRequired, assetReadAccount: state.assetReadAccount ?? { status: "address_required" } };
207
303
  }
208
304
  currentConnectionContext() {
209
305
  this.assertCurrent();
210
306
  const facts = this.options.records.readConnectionFacts(this.now()), availability = this.walletAvailability();
211
307
  const { selection: _selection, ...eligibility } = workflowConnectionEligibility({ ...facts, walletAvailability: availability });
212
- return { connections: facts.connections, walletAvailability: availability, walletObservation: facts.walletObservation,
213
- connectionConflict: eligibility.connectionConflict, manageableConnectionIds: eligibility.manageableConnectionIds,
214
- connectionReconciliationRequired: eligibility.connectionReconciliationRequired,
215
- assetReadAccount: eligibility.assetReadAccount ?? { status: "address_required" } };
308
+ return { connection: eligibility.connection, pendingOperation: facts.pendingOperation, walletAvailability: availability, walletObservation: facts.walletObservation,
309
+ requestedChains: facts.requestedChains, canDisconnect: eligibility.canDisconnect,
310
+ connectionReconciliationRequired: eligibility.connectionReconciliationRequired, assetReadAccount: eligibility.assetReadAccount ?? { status: "address_required" } };
311
+ }
312
+ async inspectRetainedSession() {
313
+ const record = this.options.records.currentConnection();
314
+ if (!record?.topic || this.walletAvailability().status !== "available" || this.observed(record.topic))
315
+ return;
316
+ const run = this.options.runtime.bind(), connectionId = record.connection.connectionId;
317
+ if (this.retainedSessionCheck?.runId === run.runId && this.retainedSessionCheck.connectionId === connectionId)
318
+ return this.retainedSessionCheck.work;
319
+ const check = { runId: run.runId, connectionId, work: Promise.resolve() };
320
+ this.retainedSessionCheck = check;
321
+ check.work = (async () => {
322
+ try {
323
+ await run.checkSession(record.topic, connectionId);
324
+ this.assertWalletAvailable(run);
325
+ }
326
+ catch {
327
+ this.report("retained_session_check");
328
+ }
329
+ finally {
330
+ if (this.retainedSessionCheck === check)
331
+ this.retainedSessionCheck = undefined;
332
+ }
333
+ })();
334
+ return check.work;
216
335
  }
217
- recoverDisconnects() {
336
+ recoverDisconnects(operationId) {
337
+ const operation = operationId ? this.options.records.operation(operationId) : undefined;
338
+ if (operationId && (!operation || operation.action !== "disconnect" || operation.status !== "running"))
339
+ return;
340
+ const target = operation?.connectionId ? this.options.records.connection(operation.connectionId) : undefined;
218
341
  const available = this.walletAvailability().status === "available";
219
- for (const record of this.options.records.connections()) {
220
- if (record.ownerId !== this.options.ownerId || record.sdkPending)
342
+ for (const record of operationId ? target ? [target] : [] : [this.options.records.currentConnection()].filter((item) => !!item)) {
343
+ if (record.ownerId !== this.options.ownerId || record.sdkPending || !record.disconnectPending)
221
344
  continue;
222
- const pending = this.options.records.pendingDisconnect(record.connection.connectionId);
345
+ const pending = operation ?? this.options.records.pendingDisconnect(record.connection.connectionId);
223
346
  if (!pending)
224
347
  continue;
225
348
  const absent = available && !!record.topic && this.observed(record.topic)?.status === "absent";
226
- this.options.records.recoverDisconnect(pending.state.cardId, record.connection.connectionId, record.connection.revision, absent);
349
+ this.options.records.recoverDisconnect(pending.operationId, record.connection.connectionId, record.connection.revision, absent);
227
350
  }
228
351
  }
229
352
  async evaluateReview(id, expectedCard, uiObservation = false, expectedAttempt) {
@@ -258,32 +381,42 @@ export class WalletWorkflow {
258
381
  }
259
382
  if (!state.session)
260
383
  return undefined;
261
- return { session: state.session, request: state.request, hasReviewInput: state.hasReviewInput, connectionConflict: state.connectionConflict, walletAvailability: state.walletAvailability, walletObservation: this.observation(),
384
+ return { session: state.session, request: state.request, hasReviewInput: state.hasReviewInput, walletAvailability: state.walletAvailability, walletObservation: this.observation(),
262
385
  connectionReconciliationRequired: state.connectionReconciliationRequired,
263
386
  progress: reviewProgress(!!state.session.preparationId, state.request, state.walletAvailability, { stopped: state.authority?.observation_stopped === 1, pending: state.authority?.lookup_pending === 1 }) };
264
387
  }
265
388
  async describe(record, uiObservation = false) {
266
389
  this.assertCurrent();
267
- if (record.state.kind === "connect")
390
+ if (record.state.kind === "connect" || record.state.kind === "disconnect") {
391
+ await this.inspectRetainedSession();
268
392
  this.reconcileConnectionContext();
393
+ }
269
394
  const evaluated = record.state.kind === "review"
270
395
  ? await this.evaluateReview(String(record.state.input.reviewSessionId), record, uiObservation)
271
396
  : this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", expectedCard: record, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
272
397
  this.assertCurrent();
273
- if (record.state.kind === "connect") {
398
+ if (record.state.kind === "connect" || record.state.kind === "disconnect") {
274
399
  const data = projectConnectionView(evaluated);
275
- const pairing = evaluated.connection && evaluated.walletAvailability.status === "available" ? this.pairings.get(evaluated.connection.connectionId) : undefined;
276
- return { evaluated, data, ...(pairing && evaluated.connection?.status === "awaiting_approval" ? { walletDisplay: pairing } : {}) };
400
+ const pairing = evaluated.operation?.action === "connect" && evaluated.operation.status === "running" && evaluated.walletAvailability.status === "available"
401
+ ? this.pairings.get(evaluated.operation.operationId) : undefined;
402
+ return { evaluated, data, ...(pairing ? { walletDisplay: pairing } : {}) };
277
403
  }
278
404
  const data = projectReviewView(evaluated);
279
405
  assertNoForbiddenMcpFields(data);
280
- if (uiObservation && evaluated.request && isInitialChainObservation(evaluated.request.requestStatus) && !evaluated.authority?.observation_stopped)
406
+ if (uiObservation && evaluated.record?.state.reason !== "cancelled" && evaluated.request &&
407
+ isInitialChainObservation(evaluated.request.requestStatus) && !evaluated.authority?.observation_stopped)
281
408
  this.observe(evaluated.request.attemptId);
282
409
  return { evaluated, data, ...(evaluated.receiptDisplay && evaluated.request ? { receiptDisplay: evaluated.receiptDisplay, displayAttemptId: evaluated.request.attemptId } : {}) };
283
410
  }
284
411
  async act(record, action) {
285
412
  this.assertCurrent();
286
413
  const runtime = this.options.runtime;
414
+ const management = walletConnectionActionSchema.safeParse(action);
415
+ if (management.success && this.options.records.recoverConnectionOperation(record, management.data))
416
+ return;
417
+ const requiresWallet = ["connect", "disconnect", "prepare_review", "request_signature"].includes(action.action);
418
+ if (requiresWallet && record.state.state === "closed")
419
+ throw new WorkflowConflict("This card was cancelled. Open a new card to start another request.");
287
420
  if ("walletRunId" in action && action.walletRunId !== runtime.runId)
288
421
  throw new WorkflowConflict("The wallet service changed. Confirm the current selection again.");
289
422
  if (action.action === "request_signature" && record.scope === "review") {
@@ -292,7 +425,7 @@ export class WalletWorkflow {
292
425
  if (recovered)
293
426
  return;
294
427
  }
295
- if (["connect", "disconnect", "use_account", "prepare_review", "request_signature"].includes(action.action))
428
+ if (requiresWallet)
296
429
  this.assertWalletAvailable();
297
430
  switch (action.action) {
298
431
  case "cancel":
@@ -301,46 +434,41 @@ export class WalletWorkflow {
301
434
  case "connect": {
302
435
  const run = runtime.bind();
303
436
  const admitted = this.options.records.admitConnection(record, action);
304
- void this.connect(admitted.connection.connectionId, run, record.state.cardId).catch(() => this.report("connection"));
305
- return;
306
- }
307
- case "use_account": {
308
- if (!action.account)
309
- throw new WorkflowConflict("No account from the connected wallet was selected.");
310
- const run = runtime.bind();
311
- await this.checkWallet(run, action.connectionId, action.account, record.state.cardId);
312
- this.assertWalletAvailable(run);
313
- this.options.records.useAccount(record, action, action.connectionId, action.account);
437
+ if (admitted.created)
438
+ void this.connect(admitted.connection.connection.connectionId, admitted.operationId, run, record.state.cardId)
439
+ .catch(() => this.report("connection"));
314
440
  return;
315
441
  }
316
442
  case "disconnect": {
317
- const run = runtime.bind();
318
- const selected = this.options.records.connection(action.connectionId);
443
+ const run = runtime.bind(), selected = this.options.records.connection(action.connectionId);
319
444
  if (!selected?.topic)
320
445
  throw new WorkflowConflict("The selected wallet connection is unavailable.");
321
- await run.checkSession(selected.topic, record.state.cardId);
446
+ await run.checkSession(selected.topic, action.operationId);
322
447
  this.assertWalletAvailable(run);
323
- const target = this.options.records.admitDisconnect(record, action, action.connectionId);
448
+ const admitted = this.options.records.admitDisconnect(record, action);
449
+ if (!admitted.created)
450
+ return;
451
+ const target = admitted.connection;
324
452
  void (async () => {
325
453
  try {
326
- if (target.topic)
327
- await run.disconnect(target.topic, record.state.cardId);
454
+ await run.disconnect(target.topic, action.operationId);
328
455
  this.assertWalletAvailable(run);
329
- this.options.records.finishDisconnect(record.state.cardId, action.connectionId, true, this.now());
456
+ this.options.records.finishDisconnect(action.operationId, action.connectionId, true, this.now());
330
457
  }
331
458
  catch {
332
459
  if (!this.stopped)
333
- this.options.records.finishDisconnect(record.state.cardId, action.connectionId, false, this.now());
460
+ this.options.records.finishDisconnect(action.operationId, action.connectionId, false, this.now());
334
461
  }
335
462
  finally {
336
- this.completeCallback({ kind: "connection", id: action.connectionId, cardId: record.state.cardId, action: "disconnect", runId: run.runId });
463
+ this.completeCallback({ kind: "connection", id: action.connectionId, cardId: record.state.cardId,
464
+ operationId: action.operationId, action: "disconnect", runId: run.runId });
337
465
  }
338
466
  })().catch(() => this.report("disconnection"));
339
467
  return;
340
468
  }
341
469
  case "stop_connection": {
342
- const id = this.options.records.manage(record, action.action);
343
- this.pairings.delete(id);
470
+ this.options.records.stopConnection(record, action.operationId);
471
+ this.pairings.delete(action.operationId);
344
472
  return;
345
473
  }
346
474
  case "prepare_review": {
@@ -403,7 +531,7 @@ export class WalletWorkflow {
403
531
  this.assertWalletAvailable();
404
532
  const current = this.options.records.connection(id);
405
533
  if (!this.options.records.isUsableConnection(id, this.now()) || !isOwnedConnectedWallet(current, this.options.ownerId, !!this.options.records.pendingDisconnect(id)) ||
406
- !current.connection.accounts.includes(account) || Date.parse(current.connection.expiresAt) <= this.now().getTime()) {
534
+ fixedChainAccount(current.connection, SUI_MAINNET_WALLET_CHAIN) !== account || Date.parse(current.connection.expiresAt) <= this.now().getTime()) {
407
535
  throw new WorkflowConflict("The selected Sui wallet connection is unavailable.");
408
536
  }
409
537
  return current.connection;
@@ -413,53 +541,80 @@ export class WalletWorkflow {
413
541
  const stored = this.options.records.connection(id);
414
542
  if (!stored?.topic)
415
543
  throw new WorkflowConflict("The selected wallet connection is unavailable.");
416
- const observed = await run.checkSession(stored.topic, operationId);
544
+ const observed = await run.checkSession(stored.topic, operationId, SUI_MAINNET_WALLET_CHAIN);
417
545
  this.assertWalletAvailable(run);
418
- if (observed.status !== "present" || !observed.session.accounts.includes(account))
546
+ if (observed.status !== "present" || !walletGrantAccount(observed.session.grants, SUI_MAINNET_WALLET_CHAIN, account))
419
547
  throw new WorkflowConflict("The selected Sui wallet account is unavailable.");
420
548
  return { connection: this.requireWallet(id, account), observed };
421
549
  }
422
- async connect(id, run, cardId) {
550
+ async connect(id, operationId, run, cardId) {
423
551
  let unrecordedOutcome = "failed";
552
+ let response, cleanupOwnsSettlement = false;
553
+ const deferCleanup = () => {
554
+ cleanupOwnsSettlement = true;
555
+ this.retainWrite(response);
556
+ };
424
557
  try {
425
- const result = await run.connect(id);
426
- let displayFailure;
558
+ const operation = this.options.records.operation(operationId);
559
+ if (operation?.input.action !== "connect")
560
+ throw new WorkflowConflict("The admitted connection scope is unavailable.");
561
+ const result = await run.connect(operation.input.chains, operationId);
562
+ let pairingPublicationFailed = false;
427
563
  try {
428
564
  this.assertWalletAvailable(run);
429
- const current = this.options.records.connection(id);
430
- if (current?.connection.status === "awaiting_approval") {
431
- this.pairings.set(id, { connectionId: id, pairingUri: result.uri, expiresAt: result.expiresAt });
432
- this.options.records.updateConnection(id, { expiresAt: result.expiresAt }, this.now());
565
+ if (this.options.records.operation(operationId)?.status === "running") {
566
+ this.options.records.pairingExpiry(operationId, result.expiresAt);
567
+ this.pairings.set(operationId, { connectionId: id, pairingUri: result.uri, expiresAt: result.expiresAt });
433
568
  }
434
569
  }
435
- catch (error) {
436
- displayFailure = error;
570
+ catch {
571
+ pairingPublicationFailed = true;
437
572
  }
438
573
  const session = await result.approval;
439
- if (displayFailure)
440
- throw displayFailure;
574
+ response = { kind: "connection_cleanup", id, operationId, cardId, runId: run.runId, session };
441
575
  this.assertWalletAvailable(run);
442
- const awaiting = this.options.records.connection(id);
443
- if (!awaiting || awaiting.connection.status !== "awaiting_approval" || Date.parse(awaiting.connection.expiresAt) <= this.now().getTime()) {
444
- await run.disconnect(session.topic, id);
576
+ const captured = this.options.records.captureConnectionSession(response, session);
577
+ if (captured !== "captured")
445
578
  return;
446
- }
447
- this.options.records.updateConnection(id, { status: "connected", accounts: session.accounts, methods: session.methods,
448
- expiresAt: session.expiresAt, walletName: session.walletName }, this.now(), session.topic, false);
579
+ const published = pairingPublicationFailed ? "cleanup_required" : this.options.records.completeConnection(response, session, this.now());
580
+ if (published === "cleanup_required")
581
+ deferCleanup();
449
582
  }
450
583
  catch (error) {
451
584
  unrecordedOutcome = error instanceof WalletRunInterruptedError ? "stopped" : error instanceof WalletUserRejectedError ? "rejected" : "failed";
452
- if (!this.stopped) {
585
+ if (response)
586
+ deferCleanup();
587
+ else if (!this.stopped) {
453
588
  const current = this.options.records.connection(id);
454
589
  if (current?.connection.status === "awaiting_approval")
455
- this.options.records.updateConnection(id, { status: error instanceof WalletRunInterruptedError ? "stopped" : error instanceof WalletUserRejectedError ? "rejected" : "failed",
456
- reason: this.walletAvailability().status !== "available" ? "The wallet connection could not be confirmed because the wallet service is unavailable." :
457
- error instanceof WalletUserRejectedError ? error.message : "Wallet connection could not be confirmed." }, this.now(), undefined, false);
590
+ this.options.records.updateConnection(id, { status: unrecordedOutcome, reason: error instanceof WalletUserRejectedError ? error.message : "Wallet connection could not be confirmed." }, this.now());
458
591
  }
459
592
  }
460
593
  finally {
461
- this.completeCallback({ kind: "connection", id, runId: run.runId, cardId, action: "connect", unrecordedOutcome });
462
- this.pairings.delete(id);
594
+ if (!cleanupOwnsSettlement)
595
+ this.completeCallback({ kind: "connection", id, runId: run.runId, cardId, operationId, action: "connect", unrecordedOutcome });
596
+ this.pairings.delete(operationId);
597
+ }
598
+ }
599
+ async cleanupConnection(response, run) {
600
+ let absent = false;
601
+ try {
602
+ this.assertWalletAvailable(run);
603
+ if (this.observed(response.session.topic)?.status !== "absent") {
604
+ try {
605
+ await run.disconnect(response.session.topic, response.operationId);
606
+ }
607
+ catch { }
608
+ }
609
+ this.assertWalletAvailable(run);
610
+ const observed = this.observed(response.session.topic);
611
+ absent = (observed?.status === "absent" ? observed : await run.checkSession(response.session.topic, response.operationId)).status === "absent";
612
+ this.assertWalletAvailable(run);
613
+ }
614
+ catch { }
615
+ finally {
616
+ this.completeCallback({ kind: "connection", id: response.id, runId: response.runId, cardId: response.cardId,
617
+ operationId: response.operationId, action: "connect", cleanup: { topic: response.session.topic, absent } });
463
618
  }
464
619
  }
465
620
  async compute(id, preparation, account, connectionId, run) {
@@ -472,9 +627,9 @@ export class WalletWorkflow {
472
627
  const computed = await computeReviewStateWithPrivateArtifacts({ reviewSessionId: id, plan: session.plans[0], account, now: this.now() }, this.options.computation);
473
628
  const { connection: currentConnection } = await this.checkWallet(run, connectionId, account, preparation);
474
629
  this.assertWalletAvailable(run);
475
- if (!this.options.records.isPreparing(id, preparation) || session.walletConnectionRevision !== currentConnection.revision)
630
+ if (!this.options.records.isPreparing(id, preparation) || session.walletGrantRevision !== currentConnection.grantRevisions[SUI_MAINNET_WALLET_CHAIN])
476
631
  return;
477
- await this.options.sessions.recordReviewStateWithArtifacts(id, computed.state, computed.privateArtifacts, this.now(), { id: preparation, connectionId, connectionRevision: currentConnection.revision });
632
+ await this.options.sessions.recordReviewStateWithArtifacts(id, computed.state, computed.privateArtifacts, this.now(), { id: preparation, connectionId, grantRevision: currentConnection.grantRevisions[SUI_MAINNET_WALLET_CHAIN] });
478
633
  }
479
634
  catch {
480
635
  if (!this.stopped)
@@ -495,18 +650,22 @@ export class WalletWorkflow {
495
650
  try {
496
651
  this.assertWalletAvailable(run);
497
652
  const connection = this.options.records.connection(connectionId);
498
- if (!connection?.topic || !connection.connection.methods.includes(SUI_SIGN_TRANSACTION_METHOD))
499
- throw new WorkflowConflict("Wallet does not support Sui sign-only requests.");
500
- const proof = await this.checkWallet(run, connectionId, request.account, request.attemptId);
653
+ if (!connection?.topic || !connectionHasSuiSigningScope(connection.connection, request.account))
654
+ throw new WorkflowConflict("The Sui signing scope is unavailable for this connection and account.");
655
+ const checkedWallet = await this.checkWallet(run, connectionId, request.account, request.attemptId);
501
656
  this.assertWalletAvailable(run);
502
657
  response = await run.sign({ topic: connection.topic, account: request.account, transactionBytesBase64: bytesBase64,
503
- sessionVersion: proof.observed.version }, request.attemptId);
658
+ chainId: SUI_MAINNET_WALLET_CHAIN, lifecycleRevision: checkedWallet.observed.lifecycleRevision, grantRevision: checkedWallet.observed.grantRevisions[SUI_MAINNET_WALLET_CHAIN] }, request.attemptId);
504
659
  this.parentSignatureWork.add(request.attemptId);
505
660
  this.assertWalletAvailable(run);
506
661
  }
507
- catch {
662
+ catch (error) {
508
663
  if (!this.stopped)
509
- this.options.records.transitionRequest(request.attemptId, "request_failed", this.now(), { reason: this.walletAvailability().status !== "available" ? "Wallet operations became unavailable before submission. The returned signature will not be submitted." : "The wallet rejected the request or did not return a signature." });
664
+ this.options.records.transitionRequest(request.attemptId, "request_failed", this.now(), {
665
+ reason: error instanceof WalletUserRejectedError ? "The wallet approval request was declined. Nothing was submitted."
666
+ : this.walletAvailability().status !== "available" ? "Wallet operations became unavailable before submission. Nothing was submitted."
667
+ : "The wallet signing request could not be completed. Nothing was submitted."
668
+ });
510
669
  return;
511
670
  }
512
671
  const current = this.options.records.request(request.attemptId);
@@ -528,7 +687,7 @@ export class WalletWorkflow {
528
687
  this.assertWalletAvailable(run);
529
688
  const current = await this.checkWallet(run, connectionId, request.account, request.attemptId);
530
689
  this.assertWalletAvailable(run);
531
- if (current.observed.version !== response.sessionVersion)
690
+ if (current.observed.lifecycleRevision !== response.lifecycleRevision || current.observed.grantRevisions[SUI_MAINNET_WALLET_CHAIN] !== response.grantRevision)
532
691
  throw new WorkflowConflict("The wallet session changed before submission. Nothing was submitted.");
533
692
  this.options.records.advanceRequestDeadlines(this.now(), request.attemptId);
534
693
  if (this.options.records.request(request.attemptId)?.requestStatus !== "awaiting_signature")
@@ -619,24 +778,14 @@ export class WalletWorkflow {
619
778
  this.assertCurrent();
620
779
  this.synchronizeWalletState();
621
780
  this.recoverDisconnects();
622
- const rows = [];
623
- for (const card of this.options.records.pendingConnectionCards(this.now())) {
624
- const evaluated = this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", expectedCard: card, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
625
- if (evaluated.record?.state.state === "closed")
626
- continue;
627
- const data = projectConnectionView(evaluated);
628
- if (data.runtimeRecovery)
629
- continue;
630
- if (!data.connection)
631
- rows.push({ cardId: card.state.cardId, status: "input_required", lastActivityAt: card.state.createdAt, progress: data.progress });
632
- else if (data.connection.status === "awaiting_approval")
633
- rows.push({ cardId: card.state.cardId,
634
- connectionId: data.connection.connectionId, status: "awaiting_approval", lastActivityAt: data.connection.updatedAt, progress: data.progress });
635
- else if (data.connection.pendingAction === "disconnect")
636
- rows.push({ cardId: card.state.cardId,
637
- connectionId: data.connection.connectionId, status: "disconnect_pending", lastActivityAt: data.connection.updatedAt, progress: data.progress });
638
- }
639
- return rows;
781
+ this.options.records.expireConnections(this.now());
782
+ return this.options.records.pendingConnectionOperations().map((operation) => ({
783
+ cardId: operation.cardId, operationId: operation.operationId,
784
+ ...(operation.connectionId ? { connectionId: operation.connectionId } : {}),
785
+ status: operation.status === "running" ? operation.action === "disconnect" ? "disconnect_pending" : "awaiting_approval" : "cleanup_pending",
786
+ lastActivityAt: operation.updatedAt,
787
+ progress: { status: "waiting" }
788
+ }));
640
789
  }
641
790
  stop() {
642
791
  if (this.stopped)