@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.
- package/README.md +21 -21
- package/dist/adapters/adapterPromptSurfaces.js +0 -20
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +0 -4
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +0 -9
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +0 -9
- package/dist/adapters/intentPlanFactories.js +0 -6
- package/dist/core/action/contractNameRegistry.js +0 -53
- package/dist/core/action/forbiddenFields.js +0 -4
- package/dist/core/action/ptbVisualizationProducer.js +0 -3
- package/dist/core/action/receiptPtbGraph.js +0 -17
- package/dist/core/action/reviewTimeSimulationEvidence.js +0 -4
- package/dist/core/action/signableAdapterContract.js +0 -5
- package/dist/core/action/suiChainReceiptMapping.js +0 -18
- package/dist/core/action/suiChainReceiptReader.js +1 -21
- package/dist/core/action/suiChainReceiptVerifier.js +0 -10
- package/dist/core/action/transactionFunding.js +0 -7
- package/dist/core/action/types.js +0 -1
- package/dist/core/action/walletReviewContractAssembler.js +0 -2
- package/dist/core/activity/localDataService.js +65 -52
- package/dist/core/activity/localDataValidation.js +1 -3
- package/dist/core/activity/sqliteActivityStore.js +24 -24
- package/dist/core/activity/sqliteActivityStoreRows.js +0 -2
- package/dist/core/activity/sqliteActivityStoreSchema.js +81 -14
- package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
- package/dist/core/read/deepbookReadHelpers.js +0 -2
- package/dist/core/read/deepbookRegistry.js +0 -19
- package/dist/core/read/deepbookUsdcChartService.js +0 -2
- package/dist/core/read/readCardResult.js +0 -2
- package/dist/core/read/readService.js +0 -19
- package/dist/core/read/readServiceTypes.js +0 -2
- package/dist/core/session/cardSession.js +1 -5
- package/dist/core/session/cardSessionStore.js +54 -7
- package/dist/core/session/reviewValidity.js +0 -3
- package/dist/core/session/sessionRecordStore.js +0 -1
- package/dist/core/session/sessionStore.js +0 -32
- package/dist/core/session/settingsFileIdentity.js +9 -0
- package/dist/core/session/settingsFileProtocol.js +20 -0
- package/dist/core/session/settingsFileStore.js +337 -0
- package/dist/core/session/settingsInputError.js +32 -0
- package/dist/core/session/settingsOperation.js +220 -0
- package/dist/core/session/settingsPreparationSql.js +13 -0
- package/dist/core/session/settingsWorkflow.js +1183 -0
- package/dist/core/session/sqliteCardStore.js +0 -2
- package/dist/core/session/sqliteSessionStore.js +0 -51
- package/dist/core/session/sqliteSettingsWorkflowStore.js +453 -0
- package/dist/core/session/sqliteTransactionMaterialStore.js +0 -11
- package/dist/core/session/sqliteWalletWorkflowStore.js +160 -66
- package/dist/core/session/status.js +2 -4
- package/dist/core/session/transactionMaterialStore.js +0 -4
- package/dist/core/session/transactionRequest.js +0 -4
- package/dist/core/session/walletConnection.js +26 -10
- package/dist/core/session/walletWorkflow.js +70 -30
- package/dist/core/session/workflowState.js +14 -14
- package/dist/core/session/workflowView.js +18 -8
- package/dist/core/suiAddress.js +0 -5
- package/dist/core/suiEndpoint.js +0 -3
- package/dist/mcp/prompts.js +0 -12
- package/dist/mcp/responseGuidance.js +10 -5
- package/dist/mcp/schemas.js +0 -3
- package/dist/mcp/serverInfo.js +3 -4
- package/dist/mcp/toolNames.js +2 -2
- package/dist/mcp/tools/account/index.js +1 -19
- package/dist/mcp/tools/action/prepareSuiActionReview.js +0 -6
- package/dist/mcp/tools/read/readToolHelpers.js +5 -4
- package/dist/mcp/tools/read/serverStatusTools.js +0 -1
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +1 -2
- package/dist/mcp/tools/session/statusTools.js +4 -5
- package/dist/mcp/tools/settings/index.js +8 -28
- package/dist/mcp-app/account.html +14 -14
- package/dist/mcp-app/account.notices.txt +1 -1
- package/dist/mcp-app/chart.html +9 -9
- package/dist/mcp-app/chart.notices.txt +1 -1
- package/dist/mcp-app/connect.html +14 -14
- package/dist/mcp-app/connect.notices.txt +1 -1
- package/dist/mcp-app/receipt.html +1298 -1463
- package/dist/mcp-app/receipt.notices.txt +226 -1204
- package/dist/mcp-app/review.html +1298 -1463
- package/dist/mcp-app/review.notices.txt +226 -1204
- package/dist/mcp-app/settings.html +3133 -0
- package/dist/mcp-app/settings.notices.txt +973 -0
- package/dist/mcp-ui/contracts.js +8 -2
- package/dist/mcp-ui/readCards.js +1 -1
- package/dist/mcp-ui/tools.js +34 -4
- package/dist/review-server/server.js +3 -51
- package/dist/runtime/application.js +41 -14
- package/dist/runtime/config.js +0 -2
- package/dist/runtime/localSettingsService.js +23 -2
- package/dist/runtime/logger.js +0 -2
- package/dist/runtime/reviewServerAcquire.js +1 -18
- package/dist/runtime/runtimeTermination.js +1 -4
- package/dist/runtime/shared/authenticatedFetch.js +2 -4
- package/dist/runtime/shared/control.js +4 -7
- package/dist/runtime/shared/dataAccess.js +0 -2
- package/dist/runtime/shared/http.js +14 -0
- package/dist/runtime/shared/mcpHttp.js +6 -6
- package/dist/runtime/shared/ownerLease.js +0 -4
- package/dist/runtime/shared/server.js +1 -2
- package/dist/runtime/shared/stdio.js +2 -5
- package/dist/runtime/smokeMainnetRead.js +0 -3
- package/dist/runtime/smokeReview.js +0 -4
- package/dist/runtime/start.js +0 -2
- package/dist/runtime/suiTransactionGraphqlMapping.js +0 -2
- package/dist/runtime/suiTransactionGraphqlQueries.js +0 -3
- package/dist/runtime/walletConnectConfig.js +0 -2
- package/dist/runtime/walletConnectStorage.js +2 -10
- package/dist/runtime/walletConnectTransport.js +0 -11
- package/dist/runtime/walletSdkChildRuntime.js +2 -6
- package/dist/runtime/walletSdkIpc.js +2 -1
- package/dist/runtime/walletSdkProcess.js +3 -8
- package/docs/AGENT_BEHAVIOR.md +45 -15
- package/docs/LOCAL_DB_ARCHITECTURE.md +145 -22
- package/docs/MCP_SETUP.md +99 -39
- package/docs/MCP_TOOLS.md +288 -24
- package/docs/SDK_API.md +63 -10
- package/docs/UTILITY_INDEX.md +9 -6
- package/docs/WALLET_CONNECTION.md +32 -10
- package/package.json +22 -8
- package/dist/core/activity/schemaVersion.js +0 -2
- package/dist/core/session/keyedRecordStore.js +0 -18
- package/dist/core/session/settingsSession.js +0 -1
- package/dist/core/session/settingsSessions.js +0 -45
- package/dist/review-app/brand-dark.svg +0 -1
- package/dist/review-app/brand-light.svg +0 -1
- package/dist/review-app/favicon.svg +0 -1
- package/dist/review-app/settings.css +0 -1
- package/dist/review-app/settings.js +0 -1
- package/dist/review-app/ui.css +0 -809
- package/dist/review-server/assets.js +0 -48
- package/dist/review-server/html.js +0 -45
- package/dist/review-server/http.js +0 -47
- package/dist/review-server/middleware/reviewToken.js +0 -7
- package/dist/review-server/settingsApi.js +0 -158
- package/docs/AGENT_DEVELOPMENT_POLICY.md +0 -815
- package/docs/FRONTEND_POLICY.md +0 -534
- package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +0 -166
- package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +0 -161
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
if (conflict)
|
|
161
|
-
for (const
|
|
162
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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 (
|
|
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" || !
|
|
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
|
-
|
|
473
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
78
|
-
connectionIds:
|
|
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:
|
|
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
|
}
|