@zktx.io/sui-mcp 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -52
- package/dist/adapters/adapterPromptSurfaces.js +1 -1
- package/dist/core/activity/localDataService.js +8 -21
- package/dist/core/activity/localDataValidation.js +22 -46
- package/dist/core/activity/sqliteAccountStore.js +46 -0
- package/dist/core/activity/sqliteActivityStore.js +65 -147
- package/dist/core/activity/sqliteActivityStoreSchema.js +42 -26
- package/dist/core/activity/transactionActivityService.js +2 -2
- package/dist/core/preferences/preferencesStore.js +2 -2
- package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
- package/dist/core/read/deepbookReadHelpers.js +1 -1
- package/dist/core/read/readResponseGuidance.js +6 -6
- package/dist/core/session/cardResult.js +41 -0
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +51 -19
- package/dist/core/session/chainAccounts.js +120 -0
- package/dist/core/session/sessionStore.js +4 -8
- package/dist/core/session/settingsFileProtocol.js +1 -1
- package/dist/core/session/settingsFileStore.js +3 -30
- package/dist/core/session/settingsOperation.js +27 -19
- package/dist/core/session/settingsWorkflow.js +161 -87
- package/dist/core/session/sqliteCardStore.js +34 -14
- package/dist/core/session/sqliteSessionStore.js +5 -5
- package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
- package/dist/core/session/sqliteTransactionRequest.js +18 -0
- package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/wait.js +6 -11
- package/dist/core/session/walletConnection.js +71 -46
- package/dist/core/session/walletConnectionOperations.js +24 -0
- package/dist/core/session/walletRuntime.js +7 -13
- package/dist/core/session/walletWorkflow.js +248 -99
- package/dist/core/session/workflowState.js +104 -38
- package/dist/core/session/workflowView.js +92 -101
- package/dist/mcp/activeAccountResponse.js +3 -0
- package/dist/mcp/prompts.js +10 -4
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/responseGuidance.js +7 -9
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/serverInfo.js +21 -19
- package/dist/mcp/toolErrors.js +5 -3
- package/dist/mcp/toolNames.js +47 -46
- package/dist/mcp/tools/action/prepareSuiActionReview.js +13 -20
- package/dist/mcp/tools/read/deepbookReadTools.js +1 -1
- package/dist/mcp/tools/read/index.js +2 -2
- package/dist/mcp/tools/read/readToolHelpers.js +9 -7
- package/dist/mcp/tools/read/serverStatusTools.js +4 -5
- package/dist/mcp/tools/read/transactionActivityTools.js +3 -3
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +2 -2
- package/dist/mcp/tools/session/statusTools.js +12 -10
- package/dist/mcp/tools/session/walletConnectionTools.js +29 -17
- package/dist/mcp/tools/settings/index.js +7 -4
- package/dist/mcp-app/account.html +7 -7
- package/dist/mcp-app/chart.html +3 -3
- package/dist/mcp-app/connect.html +13 -13
- package/dist/mcp-app/disconnect.html +3637 -0
- package/dist/mcp-app/disconnect.notices.txt +1480 -0
- package/dist/mcp-app/receipt.html +393 -393
- package/dist/mcp-app/review.html +459 -459
- package/dist/mcp-app/settings.html +7 -7
- package/dist/mcp-ui/contracts.js +3 -1
- package/dist/mcp-ui/tools.js +4 -4
- package/dist/runtime/application.js +3 -3
- package/dist/runtime/config.js +3 -6
- package/dist/runtime/localSettingsService.js +11 -8
- package/dist/runtime/shared/control.js +2 -2
- package/dist/runtime/smokeMainnetRead.js +23 -22
- package/dist/runtime/smokeReview.js +7 -7
- package/dist/runtime/start.js +1 -1
- package/dist/runtime/walletConnectTransport.js +16 -16
- package/dist/runtime/walletSdkChildRuntime.js +29 -12
- package/dist/runtime/walletSdkIpc.js +5 -4
- package/dist/runtime/walletSdkProcess.js +24 -8
- package/docs/AGENT_BEHAVIOR.md +149 -120
- package/docs/LOCAL_DB_ARCHITECTURE.md +103 -75
- package/docs/MCP_SETUP.md +365 -382
- package/docs/MCP_TOOLS.md +387 -271
- package/docs/SDK_API.md +23 -11
- package/docs/SIGNABLE_ADAPTER_CONTRACT.md +6 -6
- package/docs/TRANSACTION_ACTIVITY_LOG.md +11 -7
- package/docs/UTILITY_INDEX.md +66 -64
- package/docs/WALLET_CONNECTION.md +162 -166
- package/package.json +1 -1
- package/protocols/deepbook-margin.md +1 -1
- package/protocols/deepbook-v3.md +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ cause another signing or submission request. Request failures and unknown
|
|
|
16
16
|
outcomes are separate from independently observed chain success/failure.
|
|
17
17
|
|
|
18
18
|
Account, Receipt and Chart cards provide read-only evidence independently of
|
|
19
|
-
signing. Connect, Review and Settings also run inside the MCP host. Settings
|
|
19
|
+
signing. Connect, Disconnect, Review and Settings also run inside the MCP host. Settings
|
|
20
20
|
provides local data controls and explicit connection-service recovery. Human-readable review is not a guarantee of safety or
|
|
21
21
|
of complete interpretation of every transaction detail; the raw PTB structure
|
|
22
22
|
and addresses remain inspectable.
|
|
@@ -27,14 +27,72 @@ Users can ask ordinary questions:
|
|
|
27
27
|
- "How much are my USD-denominated assets together?"
|
|
28
28
|
- "What is the shortfall?"
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
[
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Install once with npm, then register the command in each AI app. You need [Node.js 22 or newer](https://nodejs.org/en/download), including npm. No Git checkout, build, custom installation folder, database server, Sui API key or endpoint setup is required for the normal path.
|
|
33
|
+
|
|
34
|
+
This guide describes **0.8.0**. First confirm it is published, then install that exact version:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm view @zktx.io/sui-mcp@0.8.0 version
|
|
38
|
+
npm install -g @zktx.io/sui-mcp@0.8.0
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The first command must return `0.8.0`. If it is unavailable, follow the [version availability guidance](docs/MCP_SETUP.md#npm-command-returns-404); do not drop the version and assume an older release matches this guide. Contributors can use the optional [developer checkout](docs/MCP_SETUP.md#developer-checkout-setup).
|
|
42
|
+
|
|
43
|
+
Register the installed command in your app. This is the complete default configuration for Claude Desktop; Cursor's example is in the setup guide:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"sui-mcp": {
|
|
49
|
+
"command": "sui-mcp"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Codex uses TOML in `~/.codex/config.toml` instead:
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
[mcp_servers.sui-mcp]
|
|
59
|
+
command = "sui-mcp"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Keep existing settings and other servers when adding these entries. Choose your app's walkthrough for the file location, registration and startup confirmation:
|
|
63
|
+
|
|
64
|
+
- [Codex Desktop / CLI](docs/MCP_SETUP.md#codex)
|
|
65
|
+
- [Claude Desktop](docs/MCP_SETUP.md#claude-desktop)
|
|
66
|
+
- [Claude Code](docs/MCP_SETUP.md#claude-code)
|
|
67
|
+
- [Cursor](docs/MCP_SETUP.md#cursor)
|
|
68
|
+
|
|
69
|
+
After the app loads the registration, ask: **"Use Sui MCP to show its package name, server name, version and network."** Expect `@zktx.io/sui-mcp`, `sui-mcp`, `0.8.0` and `mainnet`. Then ask: **"Use Sui MCP to list the available DeepBook pools on Sui mainnet."** This first read needs no wallet or transaction. The [first-use tutorial](docs/MCP_SETUP.md#first-use-flow) explains expected results, optional cards and wallet connection. If the app cannot find `sui-mcp`, use [Command not found](docs/MCP_SETUP.md#command-not-found).
|
|
70
|
+
|
|
71
|
+
### Using several apps at once
|
|
72
|
+
|
|
73
|
+
Codex, Claude Desktop and other local MCP apps can use the same installation at the same time on one computer under the same OS user. Register `sui-mcp` in each app. With defaults, they share the local backend, settings, records and **one current wallet connection**; no separate wallet pairing per app is needed. Changing that connection in one app affects the others, while completed cards keep their original results.
|
|
74
|
+
|
|
75
|
+
All apps must use the same package release and configuration. Updates require stopping all participating Sui MCP connections before restarting the shared backend. See [Using Multiple Apps](docs/MCP_SETUP.md#using-multiple-apps) for sharing and update conditions. Interactive wallet, Review and Settings workflows require MCP Apps card support; ordinary reads can be used without it.
|
|
76
|
+
|
|
77
|
+
### Package identity and local data
|
|
78
|
+
|
|
79
|
+
The npm package is `@zktx.io/sui-mcp`, and its repository is
|
|
80
|
+
`zktx-mcp/sui-mcp`. Confirm the requested version is available before changing
|
|
81
|
+
your client registration.
|
|
82
|
+
|
|
83
|
+
Use the full scoped package name: the unscoped `sui-mcp` package is a different
|
|
84
|
+
project. Use `sui-mcp` as the default MCP registration name. A client-defined registration
|
|
85
|
+
alias is a local label, not server identity or authorization. Compatible
|
|
86
|
+
updates preserve the current data directory. Incompatible databases and backups
|
|
87
|
+
are rejected without conversion, and files are left in place. Data from a
|
|
88
|
+
different package is not discovered or imported. See the
|
|
89
|
+
[setup guide](docs/MCP_SETUP.md#switching-the-installed-package) for installation
|
|
90
|
+
and shared-server restart instructions.
|
|
33
91
|
|
|
34
92
|
## Product Scope in Two Layers
|
|
35
93
|
|
|
36
|
-
- **Implemented:** Sui mainnet read evidence, non-signable external proposal review, and account-bound DeepBook swap review using protocol-agnostic adapters.
|
|
37
|
-
- **
|
|
94
|
+
- **Implemented:** Sui mainnet read evidence, non-signable external proposal review, and account-bound DeepBook swap review using protocol-agnostic adapters. Account, Receipt, Chart, Connect, Disconnect, Review and Settings cards use the same SQLite state and scoped permission model. The backend verifies stored material, ownership, quote/policy provenance, human-readable facts, simulation and PTB evidence before an explicit user request. WalletConnect carries bytes only between the backend and wallet; ordinary MCP responses expose facts, request status and observed execution results. Settings owns endpoint and read-address controls, local backup/import/reset, and explicit connection-service recovery. Settings operations require scoped user confirmation; ordinary MCP calls only create the card or read settings.
|
|
95
|
+
- **Safety boundaries:** no private-key custody, no MCP or AI autonomous execution, no forwarding of opaque external transaction bytes to a wallet, and no peg guarantee. The current product does not silently choose settlement tokens or routes. It does not provide fiat cash-out, P&L, tax or cost-basis support.
|
|
38
96
|
|
|
39
97
|
DeepBook provides the current scoped Sui conversion, price, and orderbook
|
|
40
98
|
evidence through the shared protocol adapter contracts. Wallet and Sui balance reads
|
|
@@ -92,22 +150,33 @@ The current release can run as a local stdio MCP server and expose mainnet Sui D
|
|
|
92
150
|
It also includes:
|
|
93
151
|
|
|
94
152
|
- a local review server bound to `127.0.0.1`;
|
|
95
|
-
- a local SQLite store for
|
|
153
|
+
- a local SQLite store for chain-qualified address history and review evidence;
|
|
96
154
|
- MCP output checks that reject forbidden executable, signing, token, seed, and
|
|
97
155
|
key-material field names from responses.
|
|
98
156
|
|
|
99
157
|
## Screens
|
|
100
158
|
|
|
101
|
-
**Wallet connection.**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
159
|
+
**Wallet connection.** One WalletConnect connection provides one fixed address
|
|
160
|
+
per approved chain. Settings chooses networks for the next connection, defaulting
|
|
161
|
+
to Sui. The wallet may return a subset. Sui, Ethereum and Solana mainnet addresses
|
|
162
|
+
are supported; financial actions remain Sui-only. A chain returning multiple
|
|
163
|
+
addresses is unavailable for that connection; there is no account selector.
|
|
164
|
+
Changing the wallet, address or network scope requires disconnecting and connecting
|
|
165
|
+
again. Connect and Disconnect are separate purpose cards. Connect starts one
|
|
166
|
+
pairing after authenticated View confirmation; Disconnect asks for one exact-target
|
|
167
|
+
confirmation. Ended cards show a fixed address-free result and accept no further
|
|
168
|
+
operation. Current addresses are available through read-only connection status.
|
|
169
|
+
Connection is not transaction approval.
|
|
170
|
+
|
|
171
|
+
**Review and result.** The Review card prepares and renews verified conditions for
|
|
172
|
+
its fixed Sui account and connection. Disconnecting and reconnecting cannot retarget an
|
|
173
|
+
existing review. A replacement connection requires a new review. Users inspect the graph,
|
|
174
|
+
amounts and costs, then choose Request wallet approval and approve in their wallet.
|
|
175
|
+
Failed preparation has an explicit retry. Receipt and completed Review show the
|
|
176
|
+
executed graph, observed asset changes and net network fee. Details supplements
|
|
177
|
+
the same facts. Missing graph or receipt data is marked unavailable.
|
|
178
|
+
|
|
179
|
+
**DeepBook USDC chart.** `ui_open_chart` opens an internal MCP Apps card with
|
|
111
180
|
official DeepBookV3 Indexer candles and volume for one selected USDC pool.
|
|
112
181
|
The saved result shows the selected pair, interval, UTC period and source time. Limited and empty results are identified.
|
|
113
182
|
|
|
@@ -156,11 +225,11 @@ These product behaviors are out of scope in the current release by design:
|
|
|
156
225
|
|
|
157
226
|
### Quote Response Limits
|
|
158
227
|
|
|
159
|
-
Quote tools such as `
|
|
160
|
-
`
|
|
228
|
+
Quote tools such as `read_quote_deepbook_action` and
|
|
229
|
+
`read_quote_deepbook_display_amount` return scoped quote facts and raw quote
|
|
161
230
|
evidence only.
|
|
162
231
|
|
|
163
|
-
Their semantics mark quote output as price evidence, not payment coverage or shortfall evidence. Coverage and shortfall answers come from `
|
|
232
|
+
Their semantics mark quote output as price evidence, not payment coverage or shortfall evidence. Coverage and shortfall answers come from `read_preview_intent_evidence.responseSummary`.
|
|
164
233
|
|
|
165
234
|
For quote responses alone, these conclusions are unsupported:
|
|
166
235
|
|
|
@@ -180,33 +249,6 @@ For quote responses alone, these conclusions are unsupported:
|
|
|
180
249
|
- cost basis is not available;
|
|
181
250
|
- actionable signing data is not available.
|
|
182
251
|
|
|
183
|
-
## Install
|
|
184
|
-
|
|
185
|
-
Use the checkout setup above for unpublished changes. A local commit or successful
|
|
186
|
-
package check does not publish a release.
|
|
187
|
-
|
|
188
|
-
This release uses npm package `@zktx.io/sui-mcp` and MCP Registry name
|
|
189
|
-
`io.github.zktx-mcp/sui-mcp`. Once the requested version is published,
|
|
190
|
-
install through your client's registry support or run `npx -y @zktx.io/sui-mcp`.
|
|
191
|
-
For per-client configuration (Claude Code, Claude Desktop, Codex, Cursor) and
|
|
192
|
-
running from a local checkout, see [docs/MCP_SETUP.md](docs/MCP_SETUP.md).
|
|
193
|
-
|
|
194
|
-
### Package identity and local data
|
|
195
|
-
|
|
196
|
-
The npm package is `@zktx.io/sui-mcp`, and its repository is
|
|
197
|
-
`zktx-mcp/sui-mcp`. Confirm the requested version is available before changing
|
|
198
|
-
your client registration.
|
|
199
|
-
|
|
200
|
-
Use the full scoped package name: the unscoped `sui-mcp` package is a different
|
|
201
|
-
project. Keep the MCP registration name `sui-mcp`, without a suffix. Compatible
|
|
202
|
-
updates preserve the current data directory. Incompatible databases and backups
|
|
203
|
-
are rejected without conversion, and files are left in place. Data from a
|
|
204
|
-
different package is not discovered or imported. See the
|
|
205
|
-
[setup guide](docs/MCP_SETUP.md#switching-the-installed-package) for installation
|
|
206
|
-
and shared-server restart instructions.
|
|
207
|
-
|
|
208
|
-
After the MCP server is connected, use [docs/MCP_SETUP.md](docs/MCP_SETUP.md#first-use-flow) for first-use setup, [docs/MCP_TOOLS.md](docs/MCP_TOOLS.md) for API fields and statuses, and [docs/AGENT_BEHAVIOR.md](docs/AGENT_BEHAVIOR.md) for user-question flow and response wording.
|
|
209
|
-
|
|
210
252
|
## Mainnet-Only Product Surface
|
|
211
253
|
|
|
212
254
|
Product docs, registry, AI responses, UX copy, and signable actions are mainnet-only.
|
|
@@ -217,11 +259,11 @@ Unsupported protocol experiments are not product functionality and are not inclu
|
|
|
217
259
|
|
|
218
260
|
The canonical MCP API reference lives in [docs/MCP_TOOLS.md](docs/MCP_TOOLS.md).
|
|
219
261
|
|
|
220
|
-
The server also exposes `
|
|
262
|
+
The server also exposes `read_get_server_status`. It returns the package version, evidence policy version, `implementedToolsCount`, and implemented tool list at runtime.
|
|
221
263
|
|
|
222
|
-
DeepBook pool-price context is exposed through `
|
|
264
|
+
DeepBook pool-price context is exposed through `read_get_deepbook_mid_price`. Use `docs/MCP_TOOLS.md` for the response fields and unsupported conclusions.
|
|
223
265
|
|
|
224
|
-
Tool names use
|
|
266
|
+
Tool names use `domain_verb_object`. Names identify an operation; its schema, card visibility and permission checks define its authority. `action_start_review` returns a `reviewSessionId` and an internal `card`; it does not request a signature, submit a transaction or return executable transaction bytes.
|
|
225
267
|
|
|
226
268
|
## Documentation Map
|
|
227
269
|
|
|
@@ -237,7 +279,7 @@ Runtime-facing MCP resources currently include:
|
|
|
237
279
|
- `protocols/deepbook-v3.md`;
|
|
238
280
|
- `protocols/deepbook-margin.md`.
|
|
239
281
|
|
|
240
|
-
Protocol resources are explanatory references. Current support is declared by `
|
|
282
|
+
Protocol resources are explanatory references. Current support is declared by `read_get_server_status`, `read_list_supported_protocols`, concrete tool schemas, and concrete tool responses, not by protocol Markdown alone.
|
|
241
283
|
|
|
242
284
|
Development-only or release-review documents can define contributor rules and checks.
|
|
243
285
|
|
|
@@ -248,7 +290,7 @@ AI client answer behavior must be mirrored in runtime-facing instructions, resou
|
|
|
248
290
|
- `docs/MCP_TOOLS.md`: API reference: tool contracts, response fields, statuses, follow-up fields, and output boundaries.
|
|
249
291
|
- `docs/AGENT_BEHAVIOR.md`: Answer playbook: user-question flows, tool selection, and response wording boundaries.
|
|
250
292
|
- `docs/WALLET_CONNECTION.md`: Wallet connection reference: active read context, private SDK ownership and user-approved transaction requests.
|
|
251
|
-
- `protocols/deepbook-v3.md`: Protocol reference only; use MCP tool responses and
|
|
293
|
+
- `protocols/deepbook-v3.md`: Protocol reference only; use MCP tool responses and read_list_supported_protocols for current support.
|
|
252
294
|
- `protocols/deepbook-margin.md`: Protocol reference only; no margin MCP read tools or signable actions are exposed in this release.
|
|
253
295
|
- `docs/TRANSACTION_ACTIVITY_LOG.md`: transaction activity evidence, storage, scan, and summary boundaries.
|
|
254
296
|
- `docs/UTILITY_INDEX.md`: manual maintainer and developer utilities. Utility rows are not MCP tools unless they explicitly name an MCP tool, and source-checkout scripts are not packaged product commands.
|
|
@@ -35,6 +35,6 @@ export const ADAPTER_PROMPT_SURFACES = [
|
|
|
35
35
|
description: 'Prepare a reviewable DeepBook mainnet swap from a one-line intent, e.g. "10 sui to usdc".',
|
|
36
36
|
intentArgDescription: 'Swap intent in one line, any language, e.g. "10 sui to usdc" or "10 수이 usdc로 환전"',
|
|
37
37
|
exampleIntents: ["10 sui to usdc", "10 수이 usdc로 환전"],
|
|
38
|
-
toolName: "
|
|
38
|
+
toolName: "action_start_review"
|
|
39
39
|
}
|
|
40
40
|
];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_WALLET_CHAINS } from "../session/chainAccounts.js";
|
|
1
2
|
import { SETTINGS_DOWNLOAD_FILE_SQL, SETTINGS_FILE_CLEANUP_SQL } from "../session/settingsFileIdentity.js";
|
|
2
3
|
import { hasUnsettledWalletWork } from "../session/sqliteWalletWorkflowStore.js";
|
|
3
4
|
import { SuiEndpointError, parseGraphqlUrl, parseGrpcUrl } from "../suiEndpoint.js";
|
|
4
5
|
import { LOCAL_DATA_EXPORT_FORMAT, LOCAL_DATA_NETWORK, LocalDataError } from "./localDataTypes.js";
|
|
5
|
-
import {
|
|
6
|
+
import { countsForPayload, invalidBackup, maxNumber, parseLocalDataEnvelope, suiGraphqlUrlFromPayload, suiGrpcUrlFromPayload } from "./localDataValidation.js";
|
|
6
7
|
export * from "./localDataTypes.js";
|
|
7
8
|
export class SqliteLocalDataService {
|
|
8
9
|
db;
|
|
@@ -32,7 +33,6 @@ export class SqliteLocalDataService {
|
|
|
32
33
|
exportedAt: now.toISOString(),
|
|
33
34
|
data: {
|
|
34
35
|
accounts: this.selectAll("accounts"),
|
|
35
|
-
activeAccountContext: this.selectAll("active_account_context"),
|
|
36
36
|
reviewSessions: this.selectAll("review_sessions"),
|
|
37
37
|
reviewStateSnapshots: this.selectAll("review_state_snapshots"),
|
|
38
38
|
reviewStatusTransitions: this.selectAll("review_status_transitions"),
|
|
@@ -55,8 +55,6 @@ export class SqliteLocalDataService {
|
|
|
55
55
|
});
|
|
56
56
|
return this.db.transaction(() => {
|
|
57
57
|
this.options.advanceRequestDeadlines(now);
|
|
58
|
-
const currentAccounts = this.selectAll("accounts");
|
|
59
|
-
const currentContext = this.selectAll("active_account_context");
|
|
60
58
|
return {
|
|
61
59
|
status: "valid",
|
|
62
60
|
format: envelope.format,
|
|
@@ -65,9 +63,6 @@ export class SqliteLocalDataService {
|
|
|
65
63
|
currentCounts: this.getDataCountsSync(),
|
|
66
64
|
incomingCounts: countsForPayload(envelope.data),
|
|
67
65
|
willReplace: true,
|
|
68
|
-
activeAccountChange: activeAccountChange(currentAccounts, currentContext, envelope.data.accounts, envelope.data.activeAccountContext),
|
|
69
|
-
currentReadAddress: activeAccountAddress(currentAccounts, currentContext),
|
|
70
|
-
incomingReadAddress: activeAccountAddress(envelope.data.accounts, envelope.data.activeAccountContext),
|
|
71
66
|
restartRequiredAfterImport: true,
|
|
72
67
|
};
|
|
73
68
|
}).immediate();
|
|
@@ -89,7 +84,6 @@ export class SqliteLocalDataService {
|
|
|
89
84
|
this.resetLocalDataTables(preserveCardId);
|
|
90
85
|
if (envelope) {
|
|
91
86
|
this.insertAccounts(envelope.data.accounts);
|
|
92
|
-
this.insertActiveAccountContext(envelope.data.activeAccountContext);
|
|
93
87
|
this.insertReviewSessions(envelope.data.reviewSessions);
|
|
94
88
|
this.insertReviewStateSnapshots(envelope.data.reviewStateSnapshots);
|
|
95
89
|
this.insertReviewRequests(envelope.data.reviewRequests);
|
|
@@ -121,6 +115,7 @@ export class SqliteLocalDataService {
|
|
|
121
115
|
}
|
|
122
116
|
else
|
|
123
117
|
this.db.prepare("DELETE FROM live_read_cards").run();
|
|
118
|
+
this.db.prepare("DELETE FROM wallet_connection_operations WHERE sdk_pending=0 AND status!='running' AND card_id NOT IN (SELECT id FROM live_read_cards)").run();
|
|
124
119
|
for (const table of [
|
|
125
120
|
"live_execution_details",
|
|
126
121
|
"live_request_authority",
|
|
@@ -135,7 +130,6 @@ export class SqliteLocalDataService {
|
|
|
135
130
|
"review_requests",
|
|
136
131
|
"review_sessions",
|
|
137
132
|
"coin_metadata_cache",
|
|
138
|
-
"active_account_context",
|
|
139
133
|
"accounts",
|
|
140
134
|
"local_settings"
|
|
141
135
|
]) {
|
|
@@ -148,6 +142,7 @@ export class SqliteLocalDataService {
|
|
|
148
142
|
VALUES (?, ?, ?)`);
|
|
149
143
|
statement.run("suiGrpcUrl", JSON.stringify(parseGrpcUrl(this.options.suiGrpcUrl)), now.toISOString());
|
|
150
144
|
statement.run("suiGraphqlUrl", JSON.stringify(parseGraphqlUrl(this.options.suiGraphqlUrl)), now.toISOString());
|
|
145
|
+
statement.run("walletConnectionChains", JSON.stringify(DEFAULT_WALLET_CHAINS), now.toISOString());
|
|
151
146
|
}
|
|
152
147
|
getDataCountsSync() {
|
|
153
148
|
return {
|
|
@@ -170,18 +165,10 @@ export class SqliteLocalDataService {
|
|
|
170
165
|
return this.db.prepare(`SELECT * FROM ${table} ORDER BY rowid ASC`).all();
|
|
171
166
|
}
|
|
172
167
|
insertAccounts(rows) {
|
|
173
|
-
const statement = this.db.prepare(`INSERT INTO accounts (id,
|
|
174
|
-
|
|
175
|
-
for (const row of rows)
|
|
176
|
-
statement.run(row.id, row.
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
insertActiveAccountContext(rows) {
|
|
180
|
-
const statement = this.db.prepare(`INSERT INTO active_account_context (id, account_id, source, set_at, wallet_name, wallet_id)
|
|
181
|
-
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
182
|
-
for (const row of rows) {
|
|
183
|
-
statement.run(row.id, row.account_id, row.source, row.set_at, row.wallet_name ?? null, row.wallet_id ?? null);
|
|
184
|
-
}
|
|
168
|
+
const statement = this.db.prepare(`INSERT INTO accounts (id,chain_id,address,first_seen_at,last_used_at,first_source,last_source)
|
|
169
|
+
VALUES (?,?,?,?,?,?,?)`);
|
|
170
|
+
for (const row of rows)
|
|
171
|
+
statement.run(row.id, row.chain_id, row.address, row.first_seen_at, row.last_used_at, row.first_source, row.last_source);
|
|
185
172
|
}
|
|
186
173
|
insertReviewSessions(rows) {
|
|
187
174
|
const statement = this.db.prepare(`INSERT INTO review_sessions
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { walletChainSchema, walletChainsSchema, normalizeChainAddress, SUI_MAINNET_WALLET_CHAIN } from "../session/chainAccounts.js";
|
|
2
|
+
import { LOCAL_SETTING_KEYS } from "../preferences/preferencesStore.js";
|
|
1
3
|
import { transactionRequestStatusSchema, transactionRequestSchema } from "../session/transactionRequest.js";
|
|
2
4
|
import { REVIEW_TRANSITION_EVENTS } from "./sqliteActivityStoreRows.js";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
|
|
5
7
|
import { parseLifecycleValidatedReviewState } from "../action/reviewStateValidation.js";
|
|
6
8
|
import { actionPlanSchema, transactionExecutionSummarySchema, internalSessionStatusSchema } from "../action/schemas.js";
|
|
7
|
-
import { parseSuiAddress } from "../suiAddress.js";
|
|
8
9
|
import { parseGraphqlUrl, parseGrpcUrl } from "../suiEndpoint.js";
|
|
9
10
|
import { EXTERNAL_ACTIVITY_TRANSACTION_DETAIL_JSON_MAX_BYTES, externalActivityTransactionDetailJsonByteLength, externalActivityTransactionDetailSchema, externalActivityTransactionDetailsReferenceOnlyAccount } from "./transactionActivityDetails.js";
|
|
10
11
|
import { LOCAL_DATA_EXPORT_FORMAT, LOCAL_DATA_NETWORK, LocalDataError } from "./localDataTypes.js";
|
|
@@ -15,20 +16,14 @@ const isoTimestamp = z.string().refine((value) => {
|
|
|
15
16
|
const nullableString = z.string().nullable();
|
|
16
17
|
const accountRowSchema = z.object({
|
|
17
18
|
id: z.number().int().positive(),
|
|
18
|
-
|
|
19
|
+
chain_id: walletChainSchema,
|
|
20
|
+
address: z.string(),
|
|
19
21
|
first_seen_at: isoTimestamp,
|
|
20
22
|
last_used_at: isoTimestamp,
|
|
21
23
|
first_source: z.enum(["wallet_connection", "review_execution"]),
|
|
22
24
|
last_source: z.enum(["wallet_connection", "review_execution"])
|
|
23
|
-
}).strict()
|
|
24
|
-
|
|
25
|
-
id: z.literal(1),
|
|
26
|
-
account_id: z.number().int().positive().nullable(),
|
|
27
|
-
source: z.enum(["wallet_connection", "cleared"]),
|
|
28
|
-
set_at: isoTimestamp,
|
|
29
|
-
wallet_name: z.string().min(1).max(200).nullish(),
|
|
30
|
-
wallet_id: z.string().min(1).max(200).nullish()
|
|
31
|
-
}).strict();
|
|
25
|
+
}).strict().refine((row) => normalizeChainAddress(row.chain_id, row.address) === row.address &&
|
|
26
|
+
(row.chain_id === SUI_MAINNET_WALLET_CHAIN || row.first_source !== "review_execution" && row.last_source !== "review_execution"), "Account identity and source must match the supported chain");
|
|
32
27
|
const reviewSessionRowSchema = z.object({
|
|
33
28
|
id: z.string().min(1),
|
|
34
29
|
plan_id: z.string().min(1),
|
|
@@ -126,13 +121,12 @@ const externalActivityTransactionRowSchema = z.object({
|
|
|
126
121
|
detail_json: z.string().nullable()
|
|
127
122
|
}).strict();
|
|
128
123
|
const localSettingRowSchema = z.object({
|
|
129
|
-
key: z.enum(
|
|
124
|
+
key: z.enum(LOCAL_SETTING_KEYS),
|
|
130
125
|
value_json: z.string(),
|
|
131
126
|
updated_at: isoTimestamp
|
|
132
127
|
}).strict();
|
|
133
128
|
const payloadSchema = z.object({
|
|
134
129
|
accounts: z.array(accountRowSchema),
|
|
135
|
-
activeAccountContext: z.array(activeAccountContextRowSchema).max(1),
|
|
136
130
|
reviewSessions: z.array(reviewSessionRowSchema),
|
|
137
131
|
reviewStateSnapshots: z.array(reviewStateSnapshotRowSchema),
|
|
138
132
|
reviewStatusTransitions: z.array(reviewStatusTransitionRowSchema),
|
|
@@ -140,7 +134,7 @@ const payloadSchema = z.object({
|
|
|
140
134
|
reviewRequests: z.array(reviewRequestRowSchema),
|
|
141
135
|
externalActivityScans: z.array(externalActivityScanRowSchema),
|
|
142
136
|
externalActivityTransactions: z.array(externalActivityTransactionRowSchema),
|
|
143
|
-
localSettings: z.array(localSettingRowSchema).length(
|
|
137
|
+
localSettings: z.array(localSettingRowSchema).length(LOCAL_SETTING_KEYS.length)
|
|
144
138
|
}).strict();
|
|
145
139
|
const envelopeSchema = z.object({
|
|
146
140
|
format: z.literal(LOCAL_DATA_EXPORT_FORMAT),
|
|
@@ -160,7 +154,7 @@ export function parseLocalDataEnvelope(input, options) {
|
|
|
160
154
|
}
|
|
161
155
|
function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
162
156
|
ensureUnique(data.accounts.map((row) => row.id), "duplicate_account_id");
|
|
163
|
-
ensureUnique(data.accounts.map((row) => row.
|
|
157
|
+
ensureUnique(data.accounts.map((row) => `${row.chain_id}:${row.address}`), "duplicate_account_address");
|
|
164
158
|
ensureUnique(data.reviewSessions.map((row) => row.id), "duplicate_review_session_id");
|
|
165
159
|
ensureUnique(data.reviewStateSnapshots.map((row) => row.id), "duplicate_review_state_snapshot_id");
|
|
166
160
|
ensureUnique(data.reviewStatusTransitions.map((row) => row.id), "duplicate_review_transition_id");
|
|
@@ -170,26 +164,18 @@ function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
|
170
164
|
ensureUnique(data.localSettings.map((row) => row.key), "duplicate_local_setting_key");
|
|
171
165
|
ensureUnique(data.reviewRequests.map((row) => row.attempt_id), "duplicate_review_request_id");
|
|
172
166
|
ensureUnique(data.reviewRequests.map((row) => `${row.review_session_id}:${row.review_revision}`), "duplicate_review_request_revision");
|
|
173
|
-
const
|
|
174
|
-
const accountAddressesById = new Map(data.accounts.map((row) => [row.id, row.
|
|
175
|
-
for (const row of data.activeAccountContext) {
|
|
176
|
-
if (row.source === "cleared" && row.account_id !== null) {
|
|
177
|
-
throw invalidBackup("invalid_active_account_context");
|
|
178
|
-
}
|
|
179
|
-
if (row.source === "wallet_connection" && (row.account_id === null || !accountIds.has(row.account_id))) {
|
|
180
|
-
throw invalidBackup("invalid_active_account_context");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
167
|
+
const suiAccountIds = new Set(data.accounts.filter((row) => row.chain_id === SUI_MAINNET_WALLET_CHAIN).map((row) => row.id));
|
|
168
|
+
const accountAddressesById = new Map(data.accounts.map((row) => [row.id, row.address]));
|
|
183
169
|
for (const row of data.reviewSessions) {
|
|
184
170
|
validateJsonColumn(row.plan_json, "plan_json", actionPlanSchema);
|
|
185
171
|
validateOptionalJsonColumn(row.intent_json, "intent_json");
|
|
186
|
-
if (row.account_id !== null && !
|
|
172
|
+
if (row.account_id !== null && !suiAccountIds.has(row.account_id)) {
|
|
187
173
|
throw invalidBackup("invalid_review_session_account");
|
|
188
174
|
}
|
|
189
175
|
}
|
|
190
176
|
const reviewSessionIds = new Set(data.reviewSessions.map((row) => row.id));
|
|
191
177
|
for (const row of data.reviewStateSnapshots) {
|
|
192
|
-
if (!reviewSessionIds.has(row.review_session_id) || !
|
|
178
|
+
if (!reviewSessionIds.has(row.review_session_id) || !suiAccountIds.has(row.account_id)) {
|
|
193
179
|
throw invalidBackup("invalid_review_state_snapshot_reference");
|
|
194
180
|
}
|
|
195
181
|
if (row.status === "blocked" && (row.blocked_reason === null || row.refresh_reason !== null)) {
|
|
@@ -242,7 +228,7 @@ function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
|
242
228
|
throw invalidBackup("invalid_review_open_time");
|
|
243
229
|
}
|
|
244
230
|
for (const row of data.reviewStatusTransitions) {
|
|
245
|
-
if (!reviewSessionIds.has(row.review_session_id) || (row.account_id !== null && !
|
|
231
|
+
if (!reviewSessionIds.has(row.review_session_id) || (row.account_id !== null && !suiAccountIds.has(row.account_id)))
|
|
246
232
|
throw invalidBackup("invalid_review_transition_reference");
|
|
247
233
|
const requestEvent = ["request_admitted", "request_status_changed", "signature_verified", "chain_result_recorded"].includes(row.event);
|
|
248
234
|
if ((row.domain === "request") !== requestEvent)
|
|
@@ -270,7 +256,7 @@ function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
|
270
256
|
}
|
|
271
257
|
const scanIds = new Set(data.externalActivityScans.map((row) => row.scan_id));
|
|
272
258
|
for (const row of data.externalActivityScans) {
|
|
273
|
-
if (!
|
|
259
|
+
if (!suiAccountIds.has(row.account_id)) {
|
|
274
260
|
throw invalidBackup("invalid_external_activity_scan_account");
|
|
275
261
|
}
|
|
276
262
|
if (row.from_timestamp !== null && row.to_timestamp !== null && row.from_timestamp > row.to_timestamp) {
|
|
@@ -281,10 +267,10 @@ function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
|
281
267
|
}
|
|
282
268
|
}
|
|
283
269
|
for (const row of data.externalActivityTransactions) {
|
|
284
|
-
if (!
|
|
270
|
+
if (!suiAccountIds.has(row.account_id) || !scanIds.has(row.first_scan_id) || !scanIds.has(row.last_scan_id)) {
|
|
285
271
|
throw invalidBackup("invalid_external_activity_transaction_reference");
|
|
286
272
|
}
|
|
287
|
-
if (row.known_sender_account_id !== null && !
|
|
273
|
+
if (row.known_sender_account_id !== null && !suiAccountIds.has(row.known_sender_account_id)) {
|
|
288
274
|
throw invalidBackup("invalid_external_activity_sender_reference");
|
|
289
275
|
}
|
|
290
276
|
if (row.detail_json !== null) {
|
|
@@ -310,6 +296,11 @@ function validatePayloadSemantics(data, validateAdapterLifecycle) {
|
|
|
310
296
|
}
|
|
311
297
|
for (const row of data.localSettings) {
|
|
312
298
|
const value = validateJsonColumn(row.value_json, "local_settings.value_json");
|
|
299
|
+
if (row.key === "walletConnectionChains") {
|
|
300
|
+
if (!walletChainsSchema.safeParse(value).success)
|
|
301
|
+
throw invalidBackup("invalid_wallet_connection_chains");
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
313
304
|
if (typeof value !== "string") {
|
|
314
305
|
throw invalidBackup(row.key === "suiGrpcUrl" ? "invalid_sui_grpc_url" : "invalid_sui_graphql_url");
|
|
315
306
|
}
|
|
@@ -417,21 +408,6 @@ export function countsForPayload(data) {
|
|
|
417
408
|
localSettings: data.localSettings.length
|
|
418
409
|
};
|
|
419
410
|
}
|
|
420
|
-
export function activeAccountChange(currentAccounts, current, incomingAccounts, incoming) {
|
|
421
|
-
const currentAccount = activeAccountAddress(currentAccounts, current);
|
|
422
|
-
const incomingAccount = activeAccountAddress(incomingAccounts, incoming);
|
|
423
|
-
if (currentAccount === incomingAccount) {
|
|
424
|
-
return "unchanged";
|
|
425
|
-
}
|
|
426
|
-
return incomingAccount === null ? "cleared" : "set";
|
|
427
|
-
}
|
|
428
|
-
export function activeAccountAddress(accounts, context) {
|
|
429
|
-
const accountId = context[0]?.account_id ?? null;
|
|
430
|
-
if (accountId === null) {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
return accounts.find((account) => account.id === accountId)?.sui_address ?? null;
|
|
434
|
-
}
|
|
435
411
|
export function maxNumber(values) {
|
|
436
412
|
return values.reduce((max, value) => Math.max(max, value), 0);
|
|
437
413
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { normalizeChainAddress, fixedChainAccount } from "../session/chainAccounts.js";
|
|
2
|
+
import { walletConnectionSchema } from "../session/walletConnection.js";
|
|
3
|
+
import { ActivityStoreError } from "./sqliteActivityStoreTypes.js";
|
|
4
|
+
export class SqliteAccountStore {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
get(chainId, address) {
|
|
10
|
+
const normalized = normalizeChainAddress(chainId, address);
|
|
11
|
+
if (!normalized)
|
|
12
|
+
throw new ActivityStoreError("Invalid chain account address");
|
|
13
|
+
const row = this.db.prepare("SELECT * FROM accounts WHERE chain_id=? AND address=?").get(chainId, normalized);
|
|
14
|
+
return row && this.account(row);
|
|
15
|
+
}
|
|
16
|
+
upsert(chainId, address, source, timestamp) {
|
|
17
|
+
const normalized = normalizeChainAddress(chainId, address);
|
|
18
|
+
if (!normalized)
|
|
19
|
+
throw new ActivityStoreError("Invalid chain account address");
|
|
20
|
+
this.db.prepare(`INSERT INTO accounts (chain_id,address,first_seen_at,last_used_at,first_source,last_source)
|
|
21
|
+
VALUES (?,?,?,?,?,?) ON CONFLICT(chain_id,address) DO UPDATE SET last_used_at=excluded.last_used_at,last_source=excluded.last_source`)
|
|
22
|
+
.run(chainId, normalized, timestamp, timestamp, source, source);
|
|
23
|
+
return this.get(chainId, normalized);
|
|
24
|
+
}
|
|
25
|
+
currentReadAccount(chainId) {
|
|
26
|
+
const row = this.db.prepare(`SELECT connection_json FROM live_wallet_connections
|
|
27
|
+
WHERE current_slot=1 AND read_address_cleared=0 AND data_epoch=(SELECT data_epoch FROM settings_control WHERE id=1)`)
|
|
28
|
+
.get();
|
|
29
|
+
if (!row)
|
|
30
|
+
return undefined;
|
|
31
|
+
const connection = walletConnectionSchema.parse(JSON.parse(row.connection_json));
|
|
32
|
+
const address = fixedChainAccount(connection, chainId);
|
|
33
|
+
if (!address)
|
|
34
|
+
return undefined;
|
|
35
|
+
const account = this.get(chainId, address);
|
|
36
|
+
if (!account)
|
|
37
|
+
return undefined;
|
|
38
|
+
return { accountId: account.id, chainId, address, revision: connection.revision, source: "wallet_connection",
|
|
39
|
+
setAt: connection.approvedAt, connectionId: connection.connectionId,
|
|
40
|
+
...(connection.walletName ? { walletName: connection.walletName } : {}) };
|
|
41
|
+
}
|
|
42
|
+
account(row) {
|
|
43
|
+
return { id: row.id, chainId: row.chain_id, address: row.address, firstSeenAt: row.first_seen_at,
|
|
44
|
+
lastUsedAt: row.last_used_at, firstSource: row.first_source, lastSource: row.last_source };
|
|
45
|
+
}
|
|
46
|
+
}
|