@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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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
|
|
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}` :
|
|
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
|
-
|
|
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 {
|
|
205
|
-
walletObservation: state.walletObservation,
|
|
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 {
|
|
213
|
-
|
|
214
|
-
connectionReconciliationRequired: eligibility.connectionReconciliationRequired,
|
|
215
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
276
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
305
|
-
|
|
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,
|
|
446
|
+
await run.checkSession(selected.topic, action.operationId);
|
|
322
447
|
this.assertWalletAvailable(run);
|
|
323
|
-
const
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
343
|
-
this.pairings.delete(
|
|
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
|
-
|
|
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.
|
|
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
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
this.pairings.set(
|
|
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
|
|
436
|
-
|
|
570
|
+
catch {
|
|
571
|
+
pairingPublicationFailed = true;
|
|
437
572
|
}
|
|
438
573
|
const session = await result.approval;
|
|
439
|
-
|
|
440
|
-
throw displayFailure;
|
|
574
|
+
response = { kind: "connection_cleanup", id, operationId, cardId, runId: run.runId, session };
|
|
441
575
|
this.assertWalletAvailable(run);
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
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
|
-
|
|
448
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
462
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
499
|
-
throw new WorkflowConflict("
|
|
500
|
-
const
|
|
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
|
-
|
|
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(), {
|
|
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.
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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)
|