@zktx.io/sui-mcp 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +89 -42
  2. package/dist/core/activity/localDataService.js +8 -21
  3. package/dist/core/activity/localDataValidation.js +22 -46
  4. package/dist/core/activity/sqliteAccountStore.js +46 -0
  5. package/dist/core/activity/sqliteActivityStore.js +65 -147
  6. package/dist/core/activity/sqliteActivityStoreSchema.js +44 -28
  7. package/dist/core/activity/sqliteActivityStoreTypes.js +9 -0
  8. package/dist/core/activity/transactionActivityService.js +2 -2
  9. package/dist/core/preferences/preferencesStore.js +2 -2
  10. package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
  11. package/dist/core/session/cardResult.js +41 -0
  12. package/dist/core/session/cardSession.js +1 -1
  13. package/dist/core/session/cardSessionStore.js +51 -19
  14. package/dist/core/session/chainAccounts.js +120 -0
  15. package/dist/core/session/sessionStore.js +4 -8
  16. package/dist/core/session/settingsFileStore.js +3 -30
  17. package/dist/core/session/settingsOperation.js +27 -19
  18. package/dist/core/session/settingsWorkflow.js +161 -87
  19. package/dist/core/session/sqliteCardStore.js +34 -14
  20. package/dist/core/session/sqliteSessionStore.js +5 -5
  21. package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
  22. package/dist/core/session/sqliteTransactionRequest.js +18 -0
  23. package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
  24. package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
  25. package/dist/core/session/status.js +2 -2
  26. package/dist/core/session/wait.js +6 -11
  27. package/dist/core/session/walletConnection.js +71 -46
  28. package/dist/core/session/walletConnectionOperations.js +24 -0
  29. package/dist/core/session/walletRuntime.js +7 -13
  30. package/dist/core/session/walletWorkflow.js +248 -99
  31. package/dist/core/session/workflowState.js +104 -38
  32. package/dist/core/session/workflowView.js +92 -101
  33. package/dist/mcp/activeAccountResponse.js +3 -0
  34. package/dist/mcp/responseGuidance.js +7 -9
  35. package/dist/mcp/serverInfo.js +11 -10
  36. package/dist/mcp/toolErrors.js +5 -3
  37. package/dist/mcp/toolNames.js +1 -0
  38. package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
  39. package/dist/mcp/tools/read/readToolHelpers.js +9 -7
  40. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  41. package/dist/mcp/tools/session/shared.js +2 -2
  42. package/dist/mcp/tools/session/statusTools.js +12 -10
  43. package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
  44. package/dist/mcp/tools/settings/index.js +7 -4
  45. package/dist/mcp-app/account.html +7 -7
  46. package/dist/mcp-app/chart.html +3 -3
  47. package/dist/mcp-app/connect.html +13 -13
  48. package/dist/mcp-app/disconnect.html +3637 -0
  49. package/dist/mcp-app/disconnect.notices.txt +1480 -0
  50. package/dist/mcp-app/receipt.html +393 -393
  51. package/dist/mcp-app/review.html +459 -459
  52. package/dist/mcp-app/settings.html +7 -7
  53. package/dist/mcp-ui/contracts.js +3 -1
  54. package/dist/mcp-ui/tools.js +4 -4
  55. package/dist/runtime/application.js +1 -1
  56. package/dist/runtime/localSettingsService.js +7 -4
  57. package/dist/runtime/runtimeTermination.js +3 -0
  58. package/dist/runtime/smokeMainnetRead.js +5 -4
  59. package/dist/runtime/walletConnectTransport.js +16 -16
  60. package/dist/runtime/walletSdkChildRuntime.js +29 -12
  61. package/dist/runtime/walletSdkIpc.js +5 -4
  62. package/dist/runtime/walletSdkProcess.js +24 -8
  63. package/docs/AGENT_BEHAVIOR.md +82 -54
  64. package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
  65. package/docs/MCP_SETUP.md +384 -347
  66. package/docs/MCP_TOOLS.md +292 -177
  67. package/docs/SDK_API.md +15 -6
  68. package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
  69. package/docs/UTILITY_INDEX.md +9 -7
  70. package/docs/WALLET_CONNECTION.md +162 -167
  71. package/package.json +1 -1
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,13 +27,77 @@ Users can ask ordinary questions:
27
27
  - "How much are my USD-denominated assets together?"
28
28
  - "What is the shortfall?"
29
29
 
30
- This document describes `@zktx.io/sui-mcp@0.7.0`. Confirm that version is
31
- published before using npm installation commands. For a local build, follow
32
- [Developer Checkout Setup](docs/MCP_SETUP.md#developer-checkout-setup).
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
+ Already used an earlier release? Follow [Updating an Existing Sui MCP Installation](docs/MCP_SETUP.md#updating-an-existing-sui-mcp-installation) first. New installations can continue below.
35
+
36
+ This guide describes **0.8.1**. First confirm it is published, then install that exact version:
37
+
38
+ ```sh
39
+ npm view @zktx.io/sui-mcp@0.8.1 version
40
+ npm install -g @zktx.io/sui-mcp@0.8.1
41
+ ```
42
+
43
+ The first command must return `0.8.1`. 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).
44
+
45
+ Register the installed command in your app. This is the complete default configuration for Claude Desktop; Cursor's example is in the setup guide:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "sui-mcp": {
51
+ "command": "sui-mcp"
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ Codex uses TOML in `~/.codex/config.toml` instead:
58
+
59
+ ```toml
60
+ [mcp_servers.sui-mcp]
61
+ command = "sui-mcp"
62
+ ```
63
+
64
+ Keep existing settings and other servers when adding these entries. Choose your app's walkthrough for the file location, registration and startup confirmation:
65
+
66
+ - [Codex Desktop / CLI](docs/MCP_SETUP.md#codex)
67
+ - [Claude Desktop](docs/MCP_SETUP.md#claude-desktop)
68
+ - [Claude Code](docs/MCP_SETUP.md#claude-code)
69
+ - [Cursor](docs/MCP_SETUP.md#cursor)
70
+
71
+ 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.1` 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).
72
+
73
+ ### Using several apps at once
74
+
75
+ 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.
76
+
77
+ 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.
78
+
79
+ ### Package identity and local data
80
+
81
+ The npm package is `@zktx.io/sui-mcp`, and its repository is
82
+ `zktx-mcp/sui-mcp`. Confirm the requested version is available before changing
83
+ your client registration.
84
+
85
+ Use the full scoped package name: the unscoped `sui-mcp` package is a different
86
+ project. Use `sui-mcp` as the default MCP registration name. A client-defined registration
87
+ alias is a local label, not server identity or authorization. This is a
88
+ pre-release: local data formats can change, and migration is not provided.
89
+ Compatible updates preserve the current data directory. Incompatible databases
90
+ and backups are rejected without conversion or automatic deletion. If startup
91
+ reports a database format mismatch, follow the [recovery instructions](docs/MCP_SETUP.md#database-format-mismatch).
92
+ You can retain the old files and use a new empty data directory, or explicitly
93
+ choose to discard the old local data and settings and start fresh with defaults.
94
+ Data from a different package is not discovered or imported. See the
95
+ [setup guide](docs/MCP_SETUP.md#switching-the-installed-package) for installation
96
+ and shared-server restart instructions.
33
97
 
34
98
  ## Product Scope in Two Layers
35
99
 
36
- - **Implemented:** Sui mainnet read evidence, non-signable external proposal review, and account-bound DeepBook swap review using protocol-agnostic adapters. Six internal cards use the same SQLite state and 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.
100
+ - **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.
37
101
  - **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
102
 
39
103
  DeepBook provides the current scoped Sui conversion, price, and orderbook
@@ -92,20 +156,31 @@ The current release can run as a local stdio MCP server and expose mainnet Sui D
92
156
  It also includes:
93
157
 
94
158
  - a local review server bound to `127.0.0.1`;
95
- - a local SQLite store for active account read context and review evidence;
159
+ - a local SQLite store for chain-qualified address history and review evidence;
96
160
  - MCP output checks that reject forbidden executable, signing, token, seed, and
97
161
  key-material field names from responses.
98
162
 
99
163
  ## Screens
100
164
 
101
- **Wallet connection.** Connect one Sui mainnet wallet and use one of its approved
102
- addresses. A manage card shows Connect wallet when no connection is active;
103
- opening it alone does not pair. A connect-intent card starts pairing after its
104
- View confirms permission and current state. Each disconnection requires the
105
- user to confirm its exact target. Connection is not transaction approval.
106
-
107
-
108
- **Review and result.** The internal Review card automatically prepares and renews verified conditions for one selected wallet/account. Users inspect its transaction graph, amounts and costs, then choose Request wallet approval and approve in their wallet. One wallet connection is used at a time; choose among its approved addresses when needed. Conflicting saved connections must be resolved through targeted disconnection. Failed preparation has an explicit retry. Receipt and completed Review show the executed transaction graph, actual asset changes and net network fee. Supplementary facts are in one Details slide; earlier estimates in Review remain distinct from actual chain results. Missing graph or receipt data is marked unavailable. Cards use a single document layout without nested card panels.
165
+ **Wallet connection.** One WalletConnect connection provides one fixed address
166
+ per approved chain. Settings chooses networks for the next connection, defaulting
167
+ to Sui. The wallet may return a subset. Sui, Ethereum and Solana mainnet addresses
168
+ are supported; financial actions remain Sui-only. A chain returning multiple
169
+ addresses is unavailable for that connection; there is no account selector.
170
+ Changing the wallet, address or network scope requires disconnecting and connecting
171
+ again. Connect and Disconnect are separate purpose cards. Connect starts one
172
+ pairing after authenticated View confirmation; Disconnect asks for one exact-target
173
+ confirmation. Ended cards show a fixed address-free result and accept no further
174
+ operation. Current addresses are available through read-only connection status.
175
+ Connection is not transaction approval.
176
+
177
+ **Review and result.** The Review card prepares and renews verified conditions for
178
+ its fixed Sui account and connection. Disconnecting and reconnecting cannot retarget an
179
+ existing review. A replacement connection requires a new review. Users inspect the graph,
180
+ amounts and costs, then choose Request wallet approval and approve in their wallet.
181
+ Failed preparation has an explicit retry. Receipt and completed Review show the
182
+ executed graph, observed asset changes and net network fee. Details supplements
183
+ the same facts. Missing graph or receipt data is marked unavailable.
109
184
 
110
185
  **DeepBook USDC chart.** `ui_open_chart` opens an internal MCP Apps card with
111
186
  official DeepBookV3 Indexer candles and volume for one selected USDC pool.
@@ -180,34 +255,6 @@ For quote responses alone, these conclusions are unsupported:
180
255
  - cost basis is not available;
181
256
  - actionable signing data is not available.
182
257
 
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. Use `sui-mcp` as the default MCP registration name. A client-defined registration
202
- alias is a local label, not server identity or authorization. Compatible
203
- updates preserve the current data directory. Incompatible databases and backups
204
- are rejected without conversion, and files are left in place. Data from a
205
- different package is not discovered or imported. See the
206
- [setup guide](docs/MCP_SETUP.md#switching-the-installed-package) for installation
207
- and shared-server restart instructions.
208
-
209
- 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.
210
-
211
258
  ## Mainnet-Only Product Surface
212
259
 
213
260
  Product docs, registry, AI responses, UX copy, and signable actions are mainnet-only.
@@ -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 { activeAccountChange, activeAccountAddress, countsForPayload, invalidBackup, maxNumber, parseLocalDataEnvelope, suiGraphqlUrlFromPayload, suiGrpcUrlFromPayload } from "./localDataValidation.js";
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, sui_address, first_seen_at, last_used_at, first_source, last_source)
174
- VALUES (?, ?, ?, ?, ?, ?)`);
175
- for (const row of rows) {
176
- statement.run(row.id, row.sui_address, row.first_seen_at, row.last_used_at, row.first_source, row.last_source);
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
- sui_address: z.string().refine((value) => parseSuiAddress(value) === value, "must be a normalized Sui address"),
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
- const activeAccountContextRowSchema = z.object({
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(["suiGrpcUrl", "suiGraphqlUrl"]),
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(2)
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.sui_address), "duplicate_account_address");
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 accountIds = new Set(data.accounts.map((row) => row.id));
174
- const accountAddressesById = new Map(data.accounts.map((row) => [row.id, row.sui_address]));
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 && !accountIds.has(row.account_id)) {
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) || !accountIds.has(row.account_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 && !accountIds.has(row.account_id)))
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 (!accountIds.has(row.account_id)) {
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 (!accountIds.has(row.account_id) || !scanIds.has(row.first_scan_id) || !scanIds.has(row.last_scan_id)) {
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 && !accountIds.has(row.known_sender_account_id)) {
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
+ }