@zktx.io/sui-mcp 0.5.0 → 0.6.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 +6 -6
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
- package/dist/core/activity/localDataService.js +71 -52
- package/dist/core/activity/localDataValidation.js +1 -3
- package/dist/core/activity/sqliteActivityStore.js +24 -16
- package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
- package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +53 -3
- package/dist/core/session/sessionStore.js +0 -17
- package/dist/core/session/settingsFileIdentity.js +11 -0
- package/dist/core/session/settingsFileProtocol.js +25 -0
- package/dist/core/session/settingsFileStore.js +346 -0
- package/dist/core/session/settingsInputError.js +34 -0
- package/dist/core/session/settingsOperation.js +224 -0
- package/dist/core/session/settingsPreparationSql.js +15 -0
- package/dist/core/session/settingsWorkflow.js +1219 -0
- package/dist/core/session/sqliteSessionStore.js +0 -40
- package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/walletConnection.js +27 -8
- package/dist/core/session/walletWorkflow.js +73 -15
- package/dist/core/session/workflowState.js +16 -8
- package/dist/core/session/workflowView.js +18 -2
- package/dist/mcp/responseGuidance.js +10 -5
- 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/read/readToolHelpers.js +5 -4
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +1 -0
- package/dist/mcp/tools/session/statusTools.js +4 -3
- 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 +33 -2
- package/dist/review-server/server.js +5 -51
- package/dist/runtime/application.js +41 -14
- package/dist/runtime/localSettingsService.js +26 -2
- package/dist/runtime/shared/authenticatedFetch.js +6 -2
- package/dist/runtime/shared/control.js +5 -6
- package/dist/runtime/shared/http.js +14 -0
- package/dist/runtime/shared/mcpHttp.js +9 -4
- package/dist/runtime/shared/server.js +1 -1
- package/dist/runtime/smokeMainnetRead.js +0 -1
- package/dist/runtime/walletConnectStorage.js +2 -4
- package/dist/runtime/walletSdkChildRuntime.js +2 -1
- package/dist/runtime/walletSdkIpc.js +2 -1
- package/dist/runtime/walletSdkProcess.js +3 -2
- package/docs/AGENT_BEHAVIOR.md +44 -10
- package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
- package/docs/FRONTEND_POLICY.md +105 -28
- package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
- package/docs/MCP_SETUP.md +99 -39
- package/docs/MCP_TOOLS.md +273 -24
- package/docs/SDK_API.md +51 -10
- package/docs/UTILITY_INDEX.md +9 -6
- package/docs/WALLET_CONNECTION.md +30 -9
- package/package.json +13 -7
- 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
|
@@ -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,12 +24,14 @@ 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
37
|
/** Purpose-specific mutations on the activity owner's connection. Each method
|
|
@@ -42,8 +44,16 @@ export class SqliteWalletWorkflowStore {
|
|
|
42
44
|
materialStore;
|
|
43
45
|
evaluateState;
|
|
44
46
|
readActiveAccount;
|
|
47
|
+
readSdkSnapshot;
|
|
45
48
|
cards;
|
|
46
|
-
|
|
49
|
+
runId;
|
|
50
|
+
setWalletRunId(runId) { this.runId = runId; }
|
|
51
|
+
assertWalletInputAllowed() {
|
|
52
|
+
if (this.db.prepare("SELECT wallet_input_blocked FROM settings_control WHERE id=1").get().wallet_input_blocked) {
|
|
53
|
+
throw new WorkflowConflict("Wallet input is blocked while the connection service is being recovered.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
constructor(db, ownerId, writeActiveAccount, clock, materialStore, evaluateState, readActiveAccount, readSdkSnapshot) {
|
|
47
57
|
this.db = db;
|
|
48
58
|
this.ownerId = ownerId;
|
|
49
59
|
this.writeActiveAccount = writeActiveAccount;
|
|
@@ -51,9 +61,10 @@ export class SqliteWalletWorkflowStore {
|
|
|
51
61
|
this.materialStore = materialStore;
|
|
52
62
|
this.evaluateState = evaluateState;
|
|
53
63
|
this.readActiveAccount = readActiveAccount;
|
|
64
|
+
this.readSdkSnapshot = readSdkSnapshot;
|
|
54
65
|
this.cards = new SqliteCardRecordStore(db);
|
|
55
66
|
}
|
|
56
|
-
|
|
67
|
+
reconcileAndEvaluate(input) { return this.evaluateState(input); }
|
|
57
68
|
currentCard(expected) {
|
|
58
69
|
const current = this.cards.get(expected.state.cardId);
|
|
59
70
|
if (!current || current.tokenHash !== expected.tokenHash || current.scope !== expected.scope || current.ownerId !== expected.ownerId ||
|
|
@@ -94,15 +105,34 @@ export class SqliteWalletWorkflowStore {
|
|
|
94
105
|
recordWalletFailure(input) {
|
|
95
106
|
// Retried by the workflow until revocation is durably recorded. The SDK
|
|
96
107
|
// run is already fenced, so a failed write cannot preserve usable authority.
|
|
97
|
-
this.invalidateWalletRun("wallet_service_lost", new Date(input.observedAt));
|
|
108
|
+
this.invalidateWalletRun("wallet_service_lost", new Date(input.observedAt), input.runId);
|
|
98
109
|
}
|
|
99
110
|
publishWalletState() { this.publishAvailableWallets(); }
|
|
100
|
-
applyWalletObservation(snapshot, previous, ready) {
|
|
111
|
+
applyWalletObservation(snapshot, previous, ready, recordReady) {
|
|
101
112
|
this.db.transaction(() => {
|
|
102
113
|
for (const record of this.connections()) {
|
|
103
|
-
if (!record.topic
|
|
114
|
+
if (!record.topic)
|
|
104
115
|
continue;
|
|
105
116
|
const observed = snapshot.sessions.find((item) => item.topic === record.topic);
|
|
117
|
+
if (record.connection.status !== "connected") {
|
|
118
|
+
// Startup can transfer management ownership; ordinary observations
|
|
119
|
+
// update the current run's facts without restoring terminal authority.
|
|
120
|
+
if ((ready || record.ownerId === this.ownerId && record.runId === snapshot.runId) &&
|
|
121
|
+
observed?.status === "present" && !record.sdkPending &&
|
|
122
|
+
!this.pendingDisconnect(record.connection.connectionId, record.ownerId)) {
|
|
123
|
+
const transferred = record.ownerId !== this.ownerId || record.runId !== snapshot.runId;
|
|
124
|
+
const changed = !isDeepStrictEqual(record.connection.accounts, observed.session.accounts) ||
|
|
125
|
+
!isDeepStrictEqual(record.connection.methods, observed.session.methods) ||
|
|
126
|
+
record.connection.expiresAt !== observed.session.expiresAt || record.connection.walletName !== observed.session.walletName;
|
|
127
|
+
if (transferred)
|
|
128
|
+
this.db.prepare("UPDATE live_wallet_connections SET owner_id=?,run_id=? WHERE id=?")
|
|
129
|
+
.run(this.ownerId, snapshot.runId, record.connection.connectionId);
|
|
130
|
+
if (transferred || changed)
|
|
131
|
+
this.updateConnection(record.connection.connectionId, { accounts: observed.session.accounts,
|
|
132
|
+
methods: observed.session.methods, expiresAt: observed.session.expiresAt, walletName: observed.session.walletName }, this.clock());
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
106
136
|
if (ready && observed?.status === "present" && !record.sdkPending && !this.pendingDisconnect(record.connection.connectionId, record.ownerId)) {
|
|
107
137
|
this.restoreConnection(observed.session, this.clock());
|
|
108
138
|
}
|
|
@@ -118,25 +148,38 @@ export class SqliteWalletWorkflowStore {
|
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
this.invalidateConflictingConnections(this.clock());
|
|
151
|
+
if (ready)
|
|
152
|
+
recordReady?.();
|
|
121
153
|
if (ready)
|
|
122
154
|
this.publishAvailableWallets();
|
|
123
155
|
}).immediate();
|
|
124
156
|
}
|
|
125
|
-
invalidateWalletRun(cause, now = this.clock()) {
|
|
157
|
+
invalidateWalletRun(cause, now = this.clock(), runId) {
|
|
126
158
|
this.db.transaction(() => {
|
|
159
|
+
const targets = new Set();
|
|
160
|
+
if (runId !== undefined) {
|
|
161
|
+
for (const row of this.db.prepare("SELECT connection_id FROM live_request_authority WHERE owner_id=? AND run_id=?")
|
|
162
|
+
.all(this.ownerId, runId))
|
|
163
|
+
targets.add(row.connection_id);
|
|
164
|
+
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")
|
|
165
|
+
.all(this.ownerId, runId))
|
|
166
|
+
targets.add(row.wallet_connection_id);
|
|
167
|
+
}
|
|
127
168
|
for (const record of this.connections()) {
|
|
128
|
-
if (record.ownerId !== this.ownerId)
|
|
169
|
+
if (record.ownerId !== this.ownerId || runId !== undefined && record.runId !== runId)
|
|
129
170
|
continue;
|
|
130
|
-
|
|
171
|
+
targets.add(record.connection.connectionId);
|
|
131
172
|
if (["connected", "awaiting_approval"].includes(record.connection.status))
|
|
132
173
|
this.updateConnection(record.connection.connectionId, {}, now);
|
|
133
174
|
}
|
|
175
|
+
for (const id of targets)
|
|
176
|
+
this.invalidateConnectionRequests(id, now, cause, runId);
|
|
134
177
|
this.publishAvailableWallets();
|
|
135
178
|
}).immediate();
|
|
136
179
|
}
|
|
137
180
|
connection(id) {
|
|
138
181
|
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,
|
|
182
|
+
return row ? { connection: walletConnectionSchema.parse(JSON.parse(row.connection_json)), ownerId: row.owner_id, ...(row.run_id === null ? {} : { runId: row.run_id }),
|
|
140
183
|
sdkPending: row.sdk_pending === 1, ...(row.topic === null ? {} : { topic: row.topic }) } : undefined;
|
|
141
184
|
}
|
|
142
185
|
connections() {
|
|
@@ -146,20 +189,49 @@ export class SqliteWalletWorkflowStore {
|
|
|
146
189
|
connectionView(connection) {
|
|
147
190
|
return { ...connection, ...(this.pendingDisconnect(connection.connectionId) ? { pendingAction: "disconnect" } : {}) };
|
|
148
191
|
}
|
|
149
|
-
|
|
150
|
-
|
|
192
|
+
connectionPresence(records, snapshot) {
|
|
193
|
+
const pendingConnectionIds = records.filter((item) => item.ownerId === this.ownerId &&
|
|
194
|
+
(item.sdkPending || !!this.pendingDisconnect(item.connection.connectionId))).map((item) => item.connection.connectionId);
|
|
195
|
+
if (!snapshot || snapshot.runId !== this.runId)
|
|
196
|
+
return { confirmed: false, connections: [], unrecorded: false, pendingConnectionIds };
|
|
197
|
+
const connections = [];
|
|
198
|
+
let unrecorded = false;
|
|
199
|
+
for (const session of snapshot.sessions) {
|
|
200
|
+
if (session.status !== "present")
|
|
201
|
+
continue;
|
|
202
|
+
const record = records.find((item) => item.topic === session.topic && item.ownerId === this.ownerId && item.runId === this.runId);
|
|
203
|
+
if (record)
|
|
204
|
+
connections.push({ connectionId: record.connection.connectionId, expiresAt: session.session.expiresAt });
|
|
205
|
+
else
|
|
206
|
+
unrecorded = true;
|
|
207
|
+
}
|
|
208
|
+
return { confirmed: true, connections, unrecorded, pendingConnectionIds };
|
|
209
|
+
}
|
|
210
|
+
readConnectionFacts(now) {
|
|
211
|
+
// A deferred read transaction reuses an outer admission transaction when
|
|
212
|
+
// present. It never repairs callbacks, advances deadlines or reserves I/O.
|
|
213
|
+
return this.db.transaction(() => {
|
|
214
|
+
const records = this.connections(), snapshot = this.readSdkSnapshot(), account = this.readActiveAccount();
|
|
215
|
+
return { evaluatedAt: now.toISOString(), activeAccount: account?.address, activeAccountConnectionId: account?.walletId,
|
|
216
|
+
connections: records.filter((item) => item.ownerId === this.ownerId).map((item) => this.connectionView(item.connection)),
|
|
217
|
+
connectionPresence: this.connectionPresence(records, snapshot),
|
|
218
|
+
...(snapshot ? { walletObservation: { runId: snapshot.runId, sequence: snapshot.sequence, observedAt: snapshot.observedAt } } : {}) };
|
|
219
|
+
}).deferred();
|
|
151
220
|
}
|
|
152
221
|
connectionSelection(now = this.clock()) {
|
|
153
|
-
|
|
222
|
+
const facts = this.readConnectionFacts(now);
|
|
223
|
+
return walletConnectionSelection(facts.connections, now.getTime(), facts.connectionPresence, facts.activeAccount ? { address: facts.activeAccount, walletId: facts.activeAccountConnectionId } : undefined);
|
|
154
224
|
}
|
|
155
225
|
isUsableConnection(id, now = this.clock()) {
|
|
156
226
|
return this.connectionSelection(now).connection?.connectionId === id;
|
|
157
227
|
}
|
|
158
228
|
invalidateConflictingConnections(now) {
|
|
159
|
-
const
|
|
160
|
-
if (conflict)
|
|
161
|
-
for (const
|
|
162
|
-
|
|
229
|
+
const selection = this.connectionSelection(now);
|
|
230
|
+
if (selection.conflict || selection.connectionReconciliationRequired)
|
|
231
|
+
for (const record of this.connections()) {
|
|
232
|
+
if (record.ownerId === this.ownerId)
|
|
233
|
+
this.invalidateConnectionRequests(record.connection.connectionId, now, selection.conflict ? "connection_conflict" : "wallet_change");
|
|
234
|
+
}
|
|
163
235
|
}
|
|
164
236
|
settleConnection(id, source) {
|
|
165
237
|
this.db.transaction(() => {
|
|
@@ -172,10 +244,13 @@ export class SqliteWalletWorkflowStore {
|
|
|
172
244
|
// visible and the original pairing's failed cleanup write arrives late.
|
|
173
245
|
const disconnect = this.db.prepare(`SELECT id FROM live_read_cards WHERE owner_id=? AND scope='connect' AND operation_id=?
|
|
174
246
|
AND json_extract(accepted_input_json,'$.action')='disconnect'`).get(this.ownerId, id);
|
|
175
|
-
if (disconnect && disconnect.id !== source.cardId)
|
|
247
|
+
if (source.action === "connect" && disconnect && disconnect.id !== source.cardId)
|
|
248
|
+
return;
|
|
249
|
+
const activeDisconnect = this.pendingDisconnect(id);
|
|
250
|
+
if (source.action === "disconnect" && activeDisconnect && activeDisconnect.state.cardId !== source.cardId)
|
|
176
251
|
return;
|
|
177
252
|
const record = this.connection(id);
|
|
178
|
-
if (!record || record.ownerId !== this.ownerId)
|
|
253
|
+
if (!record || record.ownerId !== this.ownerId || record.runId !== source.runId)
|
|
179
254
|
return;
|
|
180
255
|
if (source.action === "connect" && record.connection.status === "awaiting_approval") {
|
|
181
256
|
const status = source.unrecordedOutcome ?? "failed";
|
|
@@ -241,10 +316,10 @@ export class SqliteWalletWorkflowStore {
|
|
|
241
316
|
const connection = card.state.kind === "connect" && card.operationId ? this.connection(card.operationId)?.connection : undefined;
|
|
242
317
|
// Wallet-dependent commands already passed the workflow availability guard.
|
|
243
318
|
// This store checks DB eligibility; it does not measure SDK health.
|
|
244
|
-
const
|
|
245
|
-
const facts = {
|
|
246
|
-
authority: request ? this.authority(request.attemptId) : undefined, connection,
|
|
247
|
-
walletAvailability: { status: "available" },
|
|
319
|
+
const connectionFacts = this.readConnectionFacts(now), targetAccount = session?.account ?? connectionFacts.activeAccount;
|
|
320
|
+
const facts = { ...connectionFacts, ownerId: this.ownerId, record: card, session, request,
|
|
321
|
+
authority: request ? this.authority(request.attemptId) : undefined, connection,
|
|
322
|
+
walletAvailability: { status: "available" },
|
|
248
323
|
busyForAccount: !!targetAccount && this.busyForAccount(targetAccount, now) };
|
|
249
324
|
if (!workflowEligibility(facts).allowedActions.includes(action))
|
|
250
325
|
throw new WorkflowConflict("That action is not available right now.");
|
|
@@ -307,17 +382,51 @@ export class SqliteWalletWorkflowStore {
|
|
|
307
382
|
publishConnection(connectionId, status) {
|
|
308
383
|
// Connection updates are not completion of every operation referencing it.
|
|
309
384
|
// In particular, session_update(connected) must not finish a pending disconnect.
|
|
385
|
+
const outcome = this.connection(connectionId)?.connection;
|
|
310
386
|
this.db.prepare(`UPDATE live_read_cards SET revision=revision+1,
|
|
387
|
+
result_json=CASE WHEN state='running' AND json_extract(accepted_input_json,'$.action')='disconnect'
|
|
388
|
+
AND @disconnectTerminal THEN @outcome ELSE result_json END,
|
|
311
389
|
reason=CASE WHEN state='running' AND
|
|
312
390
|
CASE WHEN json_extract(accepted_input_json,'$.action')='disconnect'
|
|
313
391
|
THEN @disconnectTerminal ELSE @connectionTerminal END THEN 'completed' ELSE reason END,
|
|
314
392
|
state=CASE WHEN state='running' AND
|
|
315
393
|
CASE WHEN json_extract(accepted_input_json,'$.action')='disconnect'
|
|
316
394
|
THEN @disconnectTerminal ELSE @connectionTerminal END THEN 'closed' ELSE state END
|
|
317
|
-
WHERE scope='connect' AND operation_id=@id`).run({ id: connectionId,
|
|
395
|
+
WHERE scope='connect' AND operation_id=@id`).run({ id: connectionId, outcome: outcome ? JSON.stringify({ disconnectOutcome: outcome }) : null,
|
|
318
396
|
disconnectTerminal: Number(status !== "connected" && status !== "awaiting_approval"),
|
|
319
397
|
connectionTerminal: Number(status !== "awaiting_approval") });
|
|
320
398
|
}
|
|
399
|
+
recoveryImpact(runId) {
|
|
400
|
+
return walletRecoveryImpactSchema.parse({
|
|
401
|
+
runId,
|
|
402
|
+
connections: this.connections().filter((record) => record.ownerId === this.ownerId && record.runId === runId)
|
|
403
|
+
.map((record) => ({ connectionId: record.connection.connectionId, revision: record.connection.revision,
|
|
404
|
+
status: record.connection.status, accounts: record.connection.accounts, walletName: record.connection.walletName,
|
|
405
|
+
sdkPending: record.sdkPending, disconnectPending: !!this.pendingDisconnect(record.connection.connectionId) })),
|
|
406
|
+
preparations: this.db.prepare(`SELECT id,revision,review_revision,preparation_id,wallet_connection_id FROM live_review_sessions
|
|
407
|
+
WHERE owner_id=? AND preparation_run_id=? AND status!='expired' ORDER BY id`).all(this.ownerId, runId),
|
|
408
|
+
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
|
|
409
|
+
FROM review_requests r JOIN live_request_authority l ON l.attempt_id=r.attempt_id
|
|
410
|
+
WHERE l.owner_id=? AND (l.run_id=? OR r.request_status IN ('submitting','awaiting_chain_result','outcome_unknown'))
|
|
411
|
+
ORDER BY r.attempt_id`).all(this.ownerId, runId)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
recordRunExit(runId, parentProcessing = []) {
|
|
415
|
+
this.db.transaction(() => {
|
|
416
|
+
this.db.prepare(`UPDATE live_request_authority SET sdk_pending=0 WHERE owner_id=? AND run_id=? AND sdk_pending=1
|
|
417
|
+
AND attempt_id NOT IN (SELECT value FROM json_each(?))`).run(this.ownerId, runId, JSON.stringify(parentProcessing));
|
|
418
|
+
for (const record of this.connections()) {
|
|
419
|
+
if (record.ownerId !== this.ownerId || record.runId !== runId)
|
|
420
|
+
continue;
|
|
421
|
+
const pending = this.pendingDisconnect(record.connection.connectionId);
|
|
422
|
+
if (pending)
|
|
423
|
+
this.finishDisconnect(pending.state.cardId, record.connection.connectionId, false, this.clock());
|
|
424
|
+
else if (record.connection.status === "awaiting_approval")
|
|
425
|
+
this.updateConnection(record.connection.connectionId, { status: "stopped", reason: "The wallet service ended before this connection was confirmed." }, this.clock(), undefined, false);
|
|
426
|
+
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);
|
|
427
|
+
}
|
|
428
|
+
}).immediate();
|
|
429
|
+
}
|
|
321
430
|
publishAvailableWallets() {
|
|
322
431
|
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
432
|
}
|
|
@@ -328,14 +437,15 @@ export class SqliteWalletWorkflowStore {
|
|
|
328
437
|
admitConnection(expected, input) {
|
|
329
438
|
return this.db.transaction(() => {
|
|
330
439
|
const now = this.clock();
|
|
440
|
+
this.assertWalletInputAllowed();
|
|
331
441
|
this.expireConnections(now);
|
|
332
442
|
const card = this.requireInput(expected, now);
|
|
333
443
|
this.requireAction(card, "connect", now);
|
|
334
444
|
const connection = walletConnectionSchema.parse({ connectionId: randomUUID(), status: "awaiting_approval", revision: 0,
|
|
335
445
|
accounts: [], methods: [], chain: SUI_MAINNET_WALLET_CHAIN, createdAt: now.toISOString(), updatedAt: now.toISOString(),
|
|
336
446
|
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));
|
|
447
|
+
this.db.prepare("INSERT INTO live_wallet_connections (id,owner_id,status,revision,topic,sdk_pending,connection_json,run_id) VALUES (?,?,?,?,?,?,?,?)")
|
|
448
|
+
.run(connection.connectionId, this.ownerId, connection.status, 0, null, 1, JSON.stringify(connection), this.runId ?? null);
|
|
339
449
|
this.consume(card, input, connection.connectionId);
|
|
340
450
|
// Every other live input observes the admitted connection before the SDK
|
|
341
451
|
// responds, so an older selection cannot bypass the one-connection rule.
|
|
@@ -372,13 +482,17 @@ export class SqliteWalletWorkflowStore {
|
|
|
372
482
|
const existing = this.db.prepare("SELECT id FROM live_wallet_connections WHERE topic=?").get(session.topic);
|
|
373
483
|
const id = existing?.id ?? randomUUID();
|
|
374
484
|
const prior = existing ? this.connection(id) : undefined;
|
|
485
|
+
if (prior?.ownerId === this.ownerId && prior.runId === this.runId && prior.connection.status === "connected" && !prior.sdkPending &&
|
|
486
|
+
isDeepStrictEqual(prior.connection.accounts, session.accounts) && isDeepStrictEqual(prior.connection.methods, session.methods) &&
|
|
487
|
+
prior.connection.expiresAt === session.expiresAt && prior.connection.walletName === session.walletName)
|
|
488
|
+
return prior;
|
|
375
489
|
const next = walletConnectionSchema.parse({ connectionId: id, status: "connected", revision: (prior?.connection.revision ?? -1) + 1,
|
|
376
490
|
accounts: session.accounts, methods: session.methods, chain: session.chain,
|
|
377
491
|
createdAt: prior?.connection.createdAt ?? now.toISOString(), updatedAt: now.toISOString(),
|
|
378
492
|
expiresAt: session.expiresAt, walletName: session.walletName });
|
|
379
|
-
this.db.prepare(`INSERT INTO live_wallet_connections VALUES (
|
|
493
|
+
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
494
|
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));
|
|
495
|
+
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
496
|
this.publishConnection(id, next.status);
|
|
383
497
|
// Restoring the SDK session never restores a cleared read-account context.
|
|
384
498
|
return this.connection(id);
|
|
@@ -386,6 +500,7 @@ export class SqliteWalletWorkflowStore {
|
|
|
386
500
|
useAccount(expected, input, connectionId, account) {
|
|
387
501
|
this.db.transaction(() => {
|
|
388
502
|
const now = this.clock();
|
|
503
|
+
this.assertWalletInputAllowed();
|
|
389
504
|
const card = this.requireInput(expected, now), connection = this.connection(connectionId);
|
|
390
505
|
if (card.scope !== "connect" || !this.isUsableConnection(connectionId, now) || !isOwnedConnectedWallet(connection, this.ownerId, !!this.pendingDisconnect(connectionId)) ||
|
|
391
506
|
Date.parse(connection.connection.expiresAt) <= now.getTime() || !connection.connection.accounts.includes(account)) {
|
|
@@ -400,8 +515,10 @@ export class SqliteWalletWorkflowStore {
|
|
|
400
515
|
admitDisconnect(expected, input, connectionId) {
|
|
401
516
|
return this.db.transaction(() => {
|
|
402
517
|
const now = this.clock();
|
|
518
|
+
this.assertWalletInputAllowed();
|
|
403
519
|
const card = this.requireInput(expected, now), connection = this.connection(connectionId);
|
|
404
|
-
if (card.scope !== "connect" || !
|
|
520
|
+
if (card.scope !== "connect" || !connection || connection.ownerId !== this.ownerId || connection.runId !== this.runId ||
|
|
521
|
+
connection.sdkPending || !this.connectionSelection(now).manageableConnectionIds.includes(connectionId)) {
|
|
405
522
|
throw new WorkflowConflict("The wallet connection is no longer available.");
|
|
406
523
|
}
|
|
407
524
|
this.requireAction(card, "disconnect", now);
|
|
@@ -456,6 +573,7 @@ export class SqliteWalletWorkflowStore {
|
|
|
456
573
|
admitRequest(expected, input, material, selectedConnection, walletRunId) {
|
|
457
574
|
const result = this.db.transaction(() => {
|
|
458
575
|
const now = this.clock();
|
|
576
|
+
this.assertWalletInputAllowed();
|
|
459
577
|
const binding = { reviewSessionId: material.reviewSessionId, planId: material.planId, account: material.account,
|
|
460
578
|
reviewRevision: material.reviewRevision, transactionDigest: material.reviewedTransactionDigest,
|
|
461
579
|
connectionId: selectedConnection.connectionId, connectionRevision: selectedConnection.revision };
|
|
@@ -471,7 +589,8 @@ export class SqliteWalletWorkflowStore {
|
|
|
471
589
|
if (!current || !materialStillMatches(material, current, sessions.revision(current.id), artifacts, stored, now)) {
|
|
472
590
|
// Expected expiry commits its normal cleanup, but no request authority.
|
|
473
591
|
// A changed candidate is a conflict and must not invalidate newer data.
|
|
474
|
-
const evaluated = this.evaluateState({
|
|
592
|
+
const evaluated = this.evaluateState({ purpose: "review", reviewSessionId: material.reviewSessionId,
|
|
593
|
+
readTarget: this.reviewReadTarget(material.reviewSessionId, card), expectedCard: card, candidate: { session: material.review, rowRevision: material.rowRevision,
|
|
475
594
|
artifacts: material.artifacts, material }, walletAvailability: { status: "available", walletRunId } }, now);
|
|
476
595
|
return new WorkflowConflict("The transaction details used by this review are no longer current.", evaluated.events);
|
|
477
596
|
}
|
|
@@ -498,8 +617,8 @@ export class SqliteWalletWorkflowStore {
|
|
|
498
617
|
(attempt_id,review_session_id,plan_id,review_revision,account_id,transaction_digest,review_state_json,request_status,revision,created_at,updated_at)
|
|
499
618
|
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
619
|
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
|
|
620
|
+
(attempt_id,owner_id,connection_id,connection_revision,can_submit,sdk_pending,submit_pending,lookup_pending,signature_deadline,run_id)
|
|
621
|
+
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
622
|
this.db.prepare(`UPDATE live_review_sessions SET current_attempt_id=?, revision=revision+1, write_contract_version=? WHERE id=?`)
|
|
504
623
|
.run(id, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION, review.id);
|
|
505
624
|
this.db.prepare("UPDATE review_sessions SET current_attempt_id=?, updated_at=? WHERE id=?").run(id, now.toISOString(), review.id);
|
|
@@ -605,7 +724,8 @@ export class SqliteWalletWorkflowStore {
|
|
|
605
724
|
return this.db.transaction(() => {
|
|
606
725
|
const now = this.clock();
|
|
607
726
|
const request = this.request(id), authority = this.authority(id);
|
|
608
|
-
if (!request || !authority || authority.owner_id !== this.ownerId || authority.can_submit !== 1 ||
|
|
727
|
+
if (!request || !authority || authority.owner_id !== this.ownerId || authority.can_submit !== 1 || authority.run_id !== (this.runId ?? null) ||
|
|
728
|
+
this.db.prepare("SELECT wallet_input_blocked FROM settings_control WHERE id=1").get().wallet_input_blocked !== 0 ||
|
|
609
729
|
request.requestStatus !== "awaiting_signature" || Date.parse(authority.signature_deadline) <= now.getTime())
|
|
610
730
|
return false;
|
|
611
731
|
const connection = this.connection(authority.connection_id);
|
|
@@ -650,11 +770,12 @@ export class SqliteWalletWorkflowStore {
|
|
|
650
770
|
resumeObservation(id) {
|
|
651
771
|
this.db.prepare("UPDATE live_request_authority SET observation_stopped=0 WHERE attempt_id=? AND owner_id=?").run(id, this.ownerId);
|
|
652
772
|
}
|
|
653
|
-
invalidateConnectionRequests(connectionId, now, cause) {
|
|
773
|
+
invalidateConnectionRequests(connectionId, now, cause, runId) {
|
|
654
774
|
this.db.transaction(() => {
|
|
655
775
|
const rows = this.db.prepare(`SELECT * FROM live_review_sessions v WHERE wallet_connection_id=? AND owner_id=? AND status!='expired'
|
|
776
|
+
AND (? IS NULL OR v.preparation_run_id=?)
|
|
656
777
|
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);
|
|
778
|
+
.all(connectionId, this.ownerId, runId ?? null, runId ?? null);
|
|
658
779
|
for (const row of rows) {
|
|
659
780
|
const state = row.review_state_json ? JSON.parse(row.review_state_json) : undefined;
|
|
660
781
|
// Later session observations cannot rewrite why an already interrupted
|
|
@@ -676,8 +797,8 @@ export class SqliteWalletWorkflowStore {
|
|
|
676
797
|
this.publishReview(row.id);
|
|
677
798
|
}
|
|
678
799
|
}).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))
|
|
800
|
+
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=?)")
|
|
801
|
+
.all(connectionId, this.ownerId, runId ?? null, runId ?? null))
|
|
681
802
|
this.stopWaiting(row.attempt_id, now, cause);
|
|
682
803
|
}
|
|
683
804
|
cancelInput(expected) {
|
|
@@ -693,6 +814,7 @@ export class SqliteWalletWorkflowStore {
|
|
|
693
814
|
beginReviewPreparation(expected, reviewRevision, connection, account) {
|
|
694
815
|
return this.db.transaction(() => {
|
|
695
816
|
const now = this.clock();
|
|
817
|
+
this.assertWalletInputAllowed();
|
|
696
818
|
const card = this.requireInput(expected, now);
|
|
697
819
|
if (card.scope !== "review" || typeof card.state.input.reviewSessionId !== "string")
|
|
698
820
|
throw new WorkflowConflict("This card cannot update the review.");
|
|
@@ -713,8 +835,8 @@ export class SqliteWalletWorkflowStore {
|
|
|
713
835
|
if (this.busyForAccount(selection.account, now) || !currentConnection.connection.accounts.includes(selection.account))
|
|
714
836
|
throw new WorkflowConflict("The review could not be updated with the selected wallet account.");
|
|
715
837
|
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);
|
|
838
|
+
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=?`)
|
|
839
|
+
.run(preparation, this.runId ?? null, connection.connectionId, connection.revision, LIVE_REVIEW_SESSION_WRITE_CONTRACT_VERSION, id);
|
|
718
840
|
this.publishReview(id);
|
|
719
841
|
return preparation;
|
|
720
842
|
}).immediate();
|
|
@@ -798,10 +920,11 @@ export class SqliteWalletWorkflowStore {
|
|
|
798
920
|
}
|
|
799
921
|
}
|
|
800
922
|
export function hasUnsettledWalletWork(db, now = new Date()) {
|
|
801
|
-
return !!db.prepare(
|
|
923
|
+
return !!db.prepare("SELECT 1 FROM live_wallet_connections WHERE sdk_pending=1 LIMIT 1").get() ||
|
|
924
|
+
!!db.prepare(`SELECT 1 FROM live_request_authority l JOIN review_requests r ON r.attempt_id=l.attempt_id
|
|
802
925
|
WHERE l.sdk_pending=1 OR l.submit_pending=1 OR l.lookup_pending=1
|
|
803
926
|
OR (r.request_status IN (SELECT value FROM json_each(?)) AND (l.lookup_deadline IS NULL OR l.lookup_deadline>?)) LIMIT 1`)
|
|
804
|
-
|
|
927
|
+
.get(JSON.stringify(INITIAL_CHAIN_OBSERVATION_STATUSES), now.toISOString());
|
|
805
928
|
}
|
|
806
929
|
export function readStoredTransactionRequest(db, id) {
|
|
807
930
|
const row = db.prepare(`SELECT r.*, a.sui_address AS account, e.result_json FROM review_requests r
|
|
@@ -46,10 +46,10 @@ export function executionPollingHint() {
|
|
|
46
46
|
recommendedIntervalSeconds: EXECUTION_POLLING_INTERVAL_SECONDS
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
export function reviewStatusResponse({ session, request, walletAvailability, walletObservation, progress, connectionConflict }) {
|
|
49
|
+
export function reviewStatusResponse({ session, request, walletAvailability, walletObservation, progress, connectionConflict, connectionReconciliationRequired }) {
|
|
50
50
|
const pollingStatus = getExecutionPollingStatus(session, request);
|
|
51
51
|
return {
|
|
52
|
-
walletAvailability, walletObservation, progress, connectionConflict,
|
|
52
|
+
walletAvailability, walletObservation, progress, connectionConflict, connectionReconciliationRequired,
|
|
53
53
|
reviewSessionId: session.id, status: session.status, reviewRevision: session.reviewRevision,
|
|
54
54
|
account: session.account, plans: session.plans, reviewState: session.reviewState,
|
|
55
55
|
pollingStatus, statusCategory: executionStatusCategory(pollingStatus), pollingHint: executionPollingHint(),
|
|
@@ -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,18 @@ 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
|
+
// Recorded recovery facts, not proof of a currently usable wallet connection.
|
|
50
|
+
export const walletRecoveryImpactSchema = z.object({
|
|
51
|
+
runId: z.string().uuid(),
|
|
52
|
+
connections: z.array(z.object({ connectionId: z.string().min(1), revision: z.number().int().nonnegative(),
|
|
53
|
+
status: walletConnectionStatusSchema, accounts: z.array(suiAddressStringSchema), walletName: z.string().optional(),
|
|
54
|
+
sdkPending: z.boolean(), disconnectPending: z.boolean() }).strict()),
|
|
55
|
+
preparations: z.array(z.object({ id: z.string().min(1), revision: z.number().int().nonnegative(),
|
|
56
|
+
review_revision: z.number().int().nonnegative(), preparation_id: z.string().nullable(), wallet_connection_id: z.string().nullable() }).strict()),
|
|
57
|
+
requests: z.array(z.object({ attempt_id: z.string().min(1), revision: z.number().int().nonnegative(),
|
|
58
|
+
request_status: transactionRequestStatusSchema, can_submit: z.number().int().min(0).max(1),
|
|
59
|
+
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())
|
|
60
|
+
}).strict();
|
|
48
61
|
export const walletConnectionSchema = z.object({
|
|
49
62
|
connectionId: z.string().min(1),
|
|
50
63
|
status: walletConnectionStatusSchema,
|
|
@@ -63,21 +76,27 @@ export const connectionConflictSchema = z.object({
|
|
|
63
76
|
connectionIds: z.array(z.string().min(1)).min(2).refine((ids) => new Set(ids).size === ids.length)
|
|
64
77
|
}).strict();
|
|
65
78
|
export const CONNECTION_CONFLICT_MESSAGE = "More than one wallet connection is saved. Disconnect the connections you no longer need before continuing.";
|
|
79
|
+
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
80
|
export const assetReadAccountSchema = z.discriminatedUnion("status", [
|
|
67
81
|
z.object({ status: z.literal("available"), account: suiAddressStringSchema }).strict(),
|
|
68
82
|
z.object({ status: z.literal("address_required") }).strict()
|
|
69
83
|
]);
|
|
70
|
-
|
|
71
|
-
// live connection and no connection operation still awaiting its outcome.
|
|
72
|
-
export function walletConnectionSelection(connections, at, stored) {
|
|
84
|
+
export function walletConnectionSelection(connections, at, presence, stored) {
|
|
73
85
|
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
|
|
86
|
+
const pending = presence.pendingConnectionIds.length > 0 || connections.some((item) => item.pendingAction || item.status === "awaiting_approval");
|
|
87
|
+
const presentIds = presence.connections.map((item) => item.connectionId);
|
|
88
|
+
const occupied = new Set([...connected.map((item) => item.connectionId), ...presentIds]);
|
|
89
|
+
const reconciliation = presence.confirmed && presence.unrecorded && !pending;
|
|
90
|
+
const connection = presence.confirmed && !presence.unrecorded && occupied.size === 1 && connected.length === 1 &&
|
|
91
|
+
presence.connections.some((item) => item.connectionId === connected[0].connectionId && Date.parse(item.expiresAt) > at) && !pending ? connected[0] : undefined;
|
|
76
92
|
return {
|
|
77
|
-
...(
|
|
78
|
-
connectionIds:
|
|
93
|
+
...(occupied.size > 1 ? { conflict: { reason: "multiple_connections",
|
|
94
|
+
connectionIds: [...occupied].sort() } } : {}),
|
|
95
|
+
...(reconciliation ? { connectionReconciliationRequired: true } : {}),
|
|
96
|
+
manageableConnectionIds: connections.filter((item) => !presence.pendingConnectionIds.includes(item.connectionId) && !item.pendingAction && item.status !== "awaiting_approval" &&
|
|
97
|
+
(item.status === "connected" || presence.confirmed && presentIds.includes(item.connectionId))).map((item) => item.connectionId),
|
|
79
98
|
...(connection ? { connection } : {}),
|
|
80
|
-
pairingAllowed:
|
|
99
|
+
pairingAllowed: presence.confirmed && !presence.unrecorded && occupied.size === 0 && !pending,
|
|
81
100
|
assetReadAccount: connection && stored?.walletId === connection.connectionId && connection.accounts.includes(stored.address)
|
|
82
101
|
? { status: "available", account: stored.address } : { status: "address_required" }
|
|
83
102
|
};
|