@zktx.io/sui-mcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +83 -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 +42 -26
  7. package/dist/core/activity/transactionActivityService.js +2 -2
  8. package/dist/core/preferences/preferencesStore.js +2 -2
  9. package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
  10. package/dist/core/session/cardResult.js +41 -0
  11. package/dist/core/session/cardSession.js +1 -1
  12. package/dist/core/session/cardSessionStore.js +51 -19
  13. package/dist/core/session/chainAccounts.js +120 -0
  14. package/dist/core/session/sessionStore.js +4 -8
  15. package/dist/core/session/settingsFileStore.js +3 -30
  16. package/dist/core/session/settingsOperation.js +27 -19
  17. package/dist/core/session/settingsWorkflow.js +161 -87
  18. package/dist/core/session/sqliteCardStore.js +34 -14
  19. package/dist/core/session/sqliteSessionStore.js +5 -5
  20. package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
  21. package/dist/core/session/sqliteTransactionRequest.js +18 -0
  22. package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
  23. package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
  24. package/dist/core/session/status.js +2 -2
  25. package/dist/core/session/wait.js +6 -11
  26. package/dist/core/session/walletConnection.js +71 -46
  27. package/dist/core/session/walletConnectionOperations.js +24 -0
  28. package/dist/core/session/walletRuntime.js +7 -13
  29. package/dist/core/session/walletWorkflow.js +248 -99
  30. package/dist/core/session/workflowState.js +104 -38
  31. package/dist/core/session/workflowView.js +92 -101
  32. package/dist/mcp/activeAccountResponse.js +3 -0
  33. package/dist/mcp/responseGuidance.js +7 -9
  34. package/dist/mcp/serverInfo.js +11 -10
  35. package/dist/mcp/toolErrors.js +5 -3
  36. package/dist/mcp/toolNames.js +1 -0
  37. package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
  38. package/dist/mcp/tools/read/readToolHelpers.js +9 -7
  39. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  40. package/dist/mcp/tools/session/shared.js +2 -2
  41. package/dist/mcp/tools/session/statusTools.js +12 -10
  42. package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
  43. package/dist/mcp/tools/settings/index.js +7 -4
  44. package/dist/mcp-app/account.html +7 -7
  45. package/dist/mcp-app/chart.html +3 -3
  46. package/dist/mcp-app/connect.html +13 -13
  47. package/dist/mcp-app/disconnect.html +3637 -0
  48. package/dist/mcp-app/disconnect.notices.txt +1480 -0
  49. package/dist/mcp-app/receipt.html +393 -393
  50. package/dist/mcp-app/review.html +459 -459
  51. package/dist/mcp-app/settings.html +7 -7
  52. package/dist/mcp-ui/contracts.js +3 -1
  53. package/dist/mcp-ui/tools.js +4 -4
  54. package/dist/runtime/application.js +1 -1
  55. package/dist/runtime/localSettingsService.js +7 -4
  56. package/dist/runtime/smokeMainnetRead.js +5 -4
  57. package/dist/runtime/walletConnectTransport.js +16 -16
  58. package/dist/runtime/walletSdkChildRuntime.js +29 -12
  59. package/dist/runtime/walletSdkIpc.js +5 -4
  60. package/dist/runtime/walletSdkProcess.js +24 -8
  61. package/docs/AGENT_BEHAVIOR.md +82 -54
  62. package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
  63. package/docs/MCP_SETUP.md +319 -339
  64. package/docs/MCP_TOOLS.md +292 -177
  65. package/docs/SDK_API.md +15 -6
  66. package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
  67. package/docs/UTILITY_INDEX.md +9 -7
  68. package/docs/WALLET_CONNECTION.md +162 -167
  69. package/package.json +1 -1
@@ -7,6 +7,7 @@ export const CARD_RESOURCE_URIS = {
7
7
  receipt: "ui://sui-mcp/receipt.html",
8
8
  chart: "ui://sui-mcp/chart.html",
9
9
  connect: "ui://sui-mcp/connect.html",
10
+ disconnect: "ui://sui-mcp/disconnect.html",
10
11
  review: "ui://sui-mcp/review.html",
11
12
  settings: "ui://sui-mcp/settings.html"
12
13
  };
@@ -20,6 +21,7 @@ export const cardInputRequiredSchema = z.discriminatedUnion("kind", [
20
21
  z.object({ kind: z.literal("receipt"), status: z.literal("input_required"), field: z.literal("digest"), message: z.string().min(1) }).strict()
21
22
  ]);
22
23
  export const cardReferenceSchema = z.object({ cardId: z.string().min(1), permission: z.string().min(1) }).strict();
24
+ export const cardReadReferenceSchema = cardReferenceSchema;
23
25
  export const cardSubmissionSchema = cardReferenceSchema.extend({
24
26
  revision: z.number().int().nonnegative(), input: z.record(z.string(), z.unknown())
25
27
  }).strict();
@@ -27,7 +29,7 @@ export const cardReceiptDisplaySchema = receiptDisplaySchema.extend({
27
29
  cardId: z.string().min(1), revision: z.number().int().nonnegative(), attemptId: z.string().min(1).optional()
28
30
  });
29
31
  export const cardWalletDisplaySchema = walletDisplaySchema.extend({ cardId: z.string().min(1), revision: z.number().int().nonnegative() });
30
- export const settingsFileDisplaySchema = z.object({ attemptId: z.string().uuid().optional(), cardId: z.string().min(1), operationId: z.string().uuid(), fileId: z.string().uuid(),
32
+ export const settingsFileDisplaySchema = z.object({ attemptId: z.string().uuid(), cardId: z.string().min(1), operationId: z.string().uuid(), fileId: z.string().uuid(),
31
33
  offset: z.number().int().nonnegative().safe(), totalSize: z.number().int().nonnegative().safe(), sha256: z.string().regex(/^[a-f0-9]{64}$/),
32
34
  chunkBase64: z.string() }).strict();
33
35
  export const CARD_TOOLS = {
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
  import { okToolResult, errorToolResult } from "../mcp/result.js";
11
11
  import { CardError } from "../core/session/cardSessionStore.js";
12
12
  import { resolveExplicitOrActiveAccount } from "../mcp/tools/read/readToolHelpers.js";
13
- import { CARD_METADATA_KEY, CARD_DISPLAY_METADATA_KEY, WALLET_DISPLAY_METADATA_KEY, SETTINGS_FILE_METADATA_KEY, CARD_RESOURCE_PREFIX, CARD_RESOURCE_URIS, CARD_TOOLS, cardReceiptDisplaySchema, cardWalletDisplaySchema, cardReferenceSchema, cardSubmissionSchema, cardInputRequiredSchema } from "./contracts.js";
13
+ import { CARD_METADATA_KEY, CARD_DISPLAY_METADATA_KEY, WALLET_DISPLAY_METADATA_KEY, SETTINGS_FILE_METADATA_KEY, CARD_RESOURCE_PREFIX, CARD_RESOURCE_URIS, CARD_TOOLS, cardReceiptDisplaySchema, cardWalletDisplaySchema, cardReferenceSchema, cardReadReferenceSchema, cardSubmissionSchema, cardInputRequiredSchema } from "./contracts.js";
14
14
  const cardActionSchema = cardReferenceSchema.extend({ revision: z.number().int().nonnegative().safe(), input: z.union([workflowActionSchema, settingsActionSchema]) }).strict();
15
15
  export const cardMetadata = (kind) => ({
16
16
  ui: { resourceUri: CARD_RESOURCE_URIS[kind], visibility: ["model"] },
@@ -108,7 +108,7 @@ export function registerReadCards(server, deps) {
108
108
  inputSchema: chartOpenInputSchema, annotations: { readOnlyHint: true, openWorldHint: true }, _meta: cardMetadata("chart")
109
109
  }, (input) => create("chart", input));
110
110
  const actions = [
111
- { name: CARD_TOOLS.read, schema: cardReferenceSchema, run: (input) => deps.cards.store.read(cardReferenceSchema.parse(input)) },
111
+ { name: CARD_TOOLS.read, schema: cardReadReferenceSchema, run: (input) => deps.cards.store.read(cardReadReferenceSchema.parse(input)) },
112
112
  { name: CARD_TOOLS.submit, schema: cardSubmissionSchema, run: (input) => deps.cards.store.submit(cardSubmissionSchema.parse(input)) },
113
113
  { name: CARD_TOOLS.act, schema: cardActionSchema, run: (input) => deps.cards.store.act(cardActionSchema.parse(input)) }
114
114
  ];
@@ -134,7 +134,7 @@ export function registerReadCards(server, deps) {
134
134
  }
135
135
  });
136
136
  }
137
- const fileInput = cardReferenceSchema.extend({ attemptId: z.string().uuid().optional(), operationId: z.string().uuid(), fileId: z.string().uuid(), offset: z.number().int().nonnegative().safe(),
137
+ const fileInput = cardReferenceSchema.extend({ attemptId: z.string().uuid(), operationId: z.string().uuid(), fileId: z.string().uuid(), offset: z.number().int().nonnegative().safe(),
138
138
  limit: z.number().int().positive().safe().optional() }).strict();
139
139
  registerAppTool(server, CARD_TOOLS.readSettingsFile, { description: "Read a verified backup file for its current download attempt.", inputSchema: fileInput,
140
140
  _meta: { ui: { visibility: ["app"] } }, annotations: { readOnlyHint: true, openWorldHint: false }
@@ -143,7 +143,7 @@ export function registerReadCards(server, deps) {
143
143
  if (!deps.cards)
144
144
  throw new CardError("Card service is unavailable.");
145
145
  const base = okToolResult({ status: "read", fileId: input.fileId, offset: input.offset, rangeSize: Number.MAX_SAFE_INTEGER });
146
- const metadata = { ...(input.attemptId ? { attemptId: input.attemptId } : {}), cardId: input.cardId, operationId: input.operationId, fileId: input.fileId, offset: input.offset, totalSize: Number.MAX_SAFE_INTEGER,
146
+ const metadata = { attemptId: input.attemptId, cardId: input.cardId, operationId: input.operationId, fileId: input.fileId, offset: input.offset, totalSize: Number.MAX_SAFE_INTEGER,
147
147
  sha256: "0".repeat(64), chunkBase64: "" };
148
148
  base._meta = { [SETTINGS_FILE_METADATA_KEY]: metadata };
149
149
  const budget = settingsRawChunkBudget({ jsonrpc: "2.0", id: extra.mcpReq.id, result: base });
@@ -152,7 +152,7 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
152
152
  const observedAt = context.walletObservation?.observedAt;
153
153
  return { server: { name: SERVER_NAME, version: SERVER_VERSION, network: SERVER_NETWORK },
154
154
  localSettings: localSettings.getLocalSettingsSync((key) => preferencesRepository.getLocalSettingSync(key)),
155
- ...(account ? { readAddress: account.address } : {}),
155
+ ...(account ? { readAddress: account.address, readConnectionId: account.connectionId } : {}),
156
156
  assetReadAccount: context.assetReadAccount, walletAvailability: context.walletAvailability,
157
157
  ...(context.connectionReconciliationRequired ? { connectionReconciliationRequired: true } : {}),
158
158
  ...(observedAt ? { observedAt } : {}), counts: localData.getDataCountsSync() };
@@ -1,6 +1,7 @@
1
1
  import { SETTINGS_APPLIES_AFTER_RESTART, LocalSettingsError } from "../core/preferences/preferencesStore.js";
2
2
  import { DEFAULT_SUI_GRAPHQL_ENDPOINT_VERIFY_TIMEOUT_MS, DEFAULT_SUI_GRAPHQL_URL, DEFAULT_SUI_GRPC_ENDPOINT_VERIFY_TIMEOUT_MS, DEFAULT_SUI_GRPC_URL, SUI_MAINNET_CHAIN_IDENTIFIER, SuiEndpointError, parseGraphqlUrl, parseGrpcUrl, verifyMainnetGraphqlEndpoint, verifyMainnetGrpcEndpoint } from "./suiEndpoint.js";
3
3
  import { resolveSuiGraphqlUrlView, resolveSuiGrpcUrlView } from "./config.js";
4
+ import { DEFAULT_WALLET_CHAINS, walletChainsSchema } from "../core/session/chainAccounts.js";
4
5
  export class RuntimeLocalSettingsService {
5
6
  options;
6
7
  env;
@@ -33,15 +34,16 @@ export class RuntimeLocalSettingsService {
33
34
  async getLocalSettings() {
34
35
  const stored = await this.options.preferencesRepository.getSuiGrpcUrl();
35
36
  const storedGraphql = await this.options.preferencesRepository.getSuiGraphqlUrl();
36
- return this.settingsFromStored(stored?.value, storedGraphql?.value);
37
+ const networks = await this.options.preferencesRepository.getLocalSetting("walletConnectionChains");
38
+ return this.settingsFromStored(stored?.value, storedGraphql?.value, networks?.value);
37
39
  }
38
40
  getLocalSettingsSync(read) {
39
41
  const grpc = read("suiGrpcUrl"), graphql = read("suiGraphqlUrl");
40
42
  if (grpc && typeof grpc.value !== "string" || graphql && typeof graphql.value !== "string")
41
43
  throw new LocalSettingsError("internal_error", "Stored endpoints are invalid.");
42
- return this.settingsFromStored(grpc?.value, graphql?.value);
44
+ return this.settingsFromStored(grpc?.value, graphql?.value, read("walletConnectionChains")?.value);
43
45
  }
44
- settingsFromStored(stored, storedGraphql) {
46
+ settingsFromStored(stored, storedGraphql, networks) {
45
47
  const currentStoredView = resolveSuiGrpcUrlView({
46
48
  defaultSuiGrpcUrl: this.defaultSuiGrpcUrl,
47
49
  storedValue: stored,
@@ -54,7 +56,8 @@ export class RuntimeLocalSettingsService {
54
56
  });
55
57
  return {
56
58
  suiGrpcUrl: withBootEffectiveEndpoint(currentStoredView, this.bootSuiGrpcUrl),
57
- suiGraphqlUrl: withBootEffectiveEndpoint(currentStoredGraphqlView, this.bootSuiGraphqlUrl)
59
+ suiGraphqlUrl: withBootEffectiveEndpoint(currentStoredGraphqlView, this.bootSuiGraphqlUrl),
60
+ walletConnectionChains: walletChainsSchema.parse(networks ?? DEFAULT_WALLET_CHAINS)
58
61
  };
59
62
  }
60
63
  resolveEndpointChange(input) {
@@ -125,7 +125,7 @@ async function main() {
125
125
  });
126
126
  const reviewDependencies = createRuntimeReviewDependencies({ client: suiClient, chainIdentifier,
127
127
  expectedChainIdentifier: config.expectedChainIdentifier, materialStore: transactionMaterialStore, readService });
128
- await activityStore.setActiveAccount(smokeAddress, "wallet_connection", new Date(), { name: "Explicit smoke read fixture" });
128
+ await activityStore.upsertAccount(smokeAddress, "review_execution");
129
129
  server = createMcpServer({
130
130
  sessions,
131
131
  activityStore,
@@ -159,7 +159,7 @@ async function main() {
159
159
  limit: ACTIVITY_SMOKE_LIMIT
160
160
  });
161
161
  const activitySummary = await callSmokeTool(client, TOOL_NAMES.readSummarizeSuiActivityScan, {
162
- limit: ACTIVITY_SMOKE_LIMIT
162
+ account: smokeAddress, limit: ACTIVITY_SMOKE_LIMIT
163
163
  });
164
164
  assertSmokeOkStatus(wallet, TOOL_NAMES.readSummarizeWalletAssets);
165
165
  markCompleted(TOOL_NAMES.readSummarizeWalletAssets);
@@ -174,7 +174,7 @@ async function main() {
174
174
  });
175
175
  markCompleted(TOOL_NAMES.readScanSuiAccountActivity);
176
176
  activitySmoke.summary = summarizeActivitySmokePayload(activitySummary, TOOL_NAMES.readSummarizeSuiActivityScan, {
177
- expectedAccountSource: "active_account_context",
177
+ expectedAccountSource: "explicit_filter",
178
178
  requireAnalysis: true,
179
179
  expectNoDetails: true
180
180
  });
@@ -187,6 +187,7 @@ async function main() {
187
187
  });
188
188
  const functionActivitySummary = await callSmokeTool(client, TOOL_NAMES.readSummarizeSuiFunctionActivityScan, {
189
189
  function: smokeFunctionTarget,
190
+ account: smokeAddress,
190
191
  limit: ACTIVITY_SMOKE_LIMIT
191
192
  });
192
193
  functionActivitySmoke.scan = summarizeActivitySmokePayload(functionActivityScan, TOOL_NAMES.readScanSuiFunctionActivity, {
@@ -198,7 +199,7 @@ async function main() {
198
199
  });
199
200
  markCompleted(TOOL_NAMES.readScanSuiFunctionActivity);
200
201
  functionActivitySmoke.summary = summarizeActivitySmokePayload(functionActivitySummary, TOOL_NAMES.readSummarizeSuiFunctionActivityScan, {
201
- expectedAccountSource: "active_account_context",
202
+ expectedAccountSource: "explicit_filter",
202
203
  expectedRelationship: "sent",
203
204
  requireFunction: true,
204
205
  requireAnalysis: true,
@@ -1,7 +1,7 @@
1
1
  import { SignClient } from "@walletconnect/sign-client";
2
2
  import { getSdkError } from "@walletconnect/utils";
3
3
  import { z } from "zod";
4
- import { parseSuiAddress } from "../core/suiAddress.js";
4
+ import { normalizeWalletGrants, walletChainsSchema, walletGrantAccount, WALLET_CHAINS } from "../core/session/chainAccounts.js";
5
5
  import { SUI_MAINNET_WALLET_CHAIN, SUI_SIGN_TRANSACTION_METHOD, WalletUserRejectedError } from "../core/session/walletConnection.js";
6
6
  import { openWalletConnectStorage } from "./walletConnectStorage.js";
7
7
  const signedResponse = z.object({ transactionBytes: z.string().min(1), signature: z.string().min(1) }).strict();
@@ -12,16 +12,11 @@ function transportError(error) {
12
12
  ? new WalletUserRejectedError() : new Error("The wallet request could not be completed.");
13
13
  }
14
14
  function sessionValue(session) {
15
- const namespace = session.namespaces.sui ?? session.namespaces[SUI_MAINNET_WALLET_CHAIN];
16
- if (!namespace || !Number.isSafeInteger(session.expiry) || session.expiry * 1000 <= Date.now()) {
17
- throw new Error("A live Sui mainnet wallet session is required.");
15
+ if (!Number.isSafeInteger(session.expiry) || session.expiry * 1000 <= Date.now()) {
16
+ throw new Error("A live wallet session is required.");
18
17
  }
19
- const accounts = namespace.accounts.filter((account) => account.startsWith(`${SUI_MAINNET_WALLET_CHAIN}:`))
20
- .map((account) => parseSuiAddress(account.slice(SUI_MAINNET_WALLET_CHAIN.length + 1)));
21
- if (accounts.length === 0 || accounts.some((account) => !account))
22
- throw new Error("The wallet did not approve a Sui mainnet account.");
23
- return { topic: session.topic, accounts: [...new Set(accounts)], methods: [...namespace.methods],
24
- chain: SUI_MAINNET_WALLET_CHAIN, expiresAt: new Date(session.expiry * 1000).toISOString(), walletName: session.peer.metadata.name };
18
+ return { topic: session.topic, grants: normalizeWalletGrants(session.namespaces),
19
+ expiresAt: new Date(session.expiry * 1000).toISOString(), walletName: session.peer.metadata.name };
25
20
  }
26
21
  export async function createWalletConnectTransport(options) {
27
22
  if (!/^[0-9a-f]{32}$/i.test(options.projectId))
@@ -37,8 +32,9 @@ export async function createWalletConnectTransport(options) {
37
32
  listener(event.topic); };
38
33
  const selectionChanged = (event) => {
39
34
  if (!stopped && ["accountsChanged", "chainChanged"].includes(event.params.event.name)) {
35
+ const chain = WALLET_CHAINS.find((id) => id === event.params.chainId);
40
36
  for (const listener of listeners)
41
- listener(event.topic, true);
37
+ listener(event.topic, chain ? [chain] : WALLET_CHAINS);
42
38
  }
43
39
  };
44
40
  client.on("session_update", changed);
@@ -76,11 +72,15 @@ export async function createWalletConnectTransport(options) {
76
72
  }
77
73
  return client.session.getAll().flatMap((session) => { const value = current(session.topic); return value ? [value] : []; });
78
74
  },
79
- async connect() {
75
+ async connect(chains) {
80
76
  requireActive();
81
- const connecting = await client.connect({ requiredNamespaces: { sui: {
82
- chains: [SUI_MAINNET_WALLET_CHAIN], methods: [SUI_SIGN_TRANSACTION_METHOD], events: ["accountsChanged", "chainChanged"]
83
- } } });
77
+ const scope = walletChainsSchema.parse(chains);
78
+ if (!scope.length)
79
+ throw new Error("Choose wallet networks in Settings before connecting.");
80
+ const optionalNamespaces = Object.fromEntries(scope.map((chain) => [chain.split(":")[0], { chains: [chain],
81
+ methods: chain === SUI_MAINNET_WALLET_CHAIN ? [SUI_SIGN_TRANSACTION_METHOD] : [],
82
+ events: chain === SUI_MAINNET_WALLET_CHAIN ? ["accountsChanged", "chainChanged"] : [] }]));
83
+ const connecting = await client.connect({ requiredNamespaces: {}, optionalNamespaces });
84
84
  const approval = connecting.approval().then(sessionValue).catch((error) => { throw transportError(error); });
85
85
  void approval.catch(() => undefined);
86
86
  if (!connecting.uri)
@@ -95,7 +95,7 @@ export async function createWalletConnectTransport(options) {
95
95
  async sign(input) {
96
96
  requireActive();
97
97
  const session = current(input.topic);
98
- if (!session?.accounts.includes(input.account) || !session.methods.includes(SUI_SIGN_TRANSACTION_METHOD)) {
98
+ if (!session || !walletGrantAccount(session.grants, SUI_MAINNET_WALLET_CHAIN, input.account)?.methods.includes(SUI_SIGN_TRANSACTION_METHOD)) {
99
99
  throw new Error("This wallet session cannot sign for the selected Sui account.");
100
100
  }
101
101
  return signedResponse.parse(await client.request({ topic: input.topic, chainId: SUI_MAINNET_WALLET_CHAIN,
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from "node:util";
4
4
  import { WalletUserRejectedError } from "../core/session/walletConnection.js";
5
5
  import { acquireDataDirectoryOwner } from "./shared/ownerLease.js";
6
6
  import { walletCommandSchema, walletEventSchema } from "./walletSdkIpc.js";
7
+ import { WALLET_CHAINS, comparableGrantScope, initialGrantRevisions, walletGrantAccount } from "../core/session/chainAccounts.js";
7
8
  export function runWalletSdkChild(createTransport) {
8
9
  let init, transport;
9
10
  let sequence = 0, lastRequest = 0, ready = false, exiting = false;
@@ -42,15 +43,23 @@ export function runWalletSdkChild(createTransport) {
42
43
  exit();
43
44
  return;
44
45
  }
45
- const inspect = (value, changedTopic) => {
46
+ const inspect = (value, change) => {
46
47
  const prior = sessions.get(value.topic);
47
- const priorValue = prior && (prior.status === "present" ? { topic: prior.topic, status: prior.status, session: prior.session } : { topic: prior.topic, status: prior.status });
48
- const version = (prior?.version ?? 0) + Number(!prior || !isDeepStrictEqual(priorValue, value) || changedTopic === value.topic);
49
- const next = { ...value, version };
48
+ const lifecycleChanged = !prior || prior.status !== value.status;
49
+ const lifecycleRevision = (prior?.lifecycleRevision ?? 0) + Number(lifecycleChanged);
50
+ const grantRevisions = { ...(prior?.grantRevisions ?? initialGrantRevisions()) };
51
+ for (const chain of WALLET_CHAINS) {
52
+ const before = prior?.status === "present" ? prior.session.grants.find((grant) => grant.chainId === chain) : undefined;
53
+ const after = value.status === "present" ? value.session.grants.find((grant) => grant.chainId === chain) : undefined;
54
+ if (lifecycleChanged || !isDeepStrictEqual(comparableGrantScope(before), comparableGrantScope(after)) ||
55
+ change?.topic === value.topic && change.chains.includes(chain))
56
+ grantRevisions[chain]++;
57
+ }
58
+ const next = { ...value, lifecycleRevision, grantRevisions };
50
59
  sessions.set(value.topic, next);
51
60
  return next;
52
61
  };
53
- const snapshot = (isReady, changedTopic, checkTopic) => {
62
+ const snapshot = (isReady, change, checkTopic) => {
54
63
  try {
55
64
  if (!ownership || !transport)
56
65
  throw new Error("Wallet storage ownership is unavailable.");
@@ -61,7 +70,7 @@ export function runWalletSdkChild(createTransport) {
61
70
  if (checkTopic && !values.some((item) => item.topic === checkTopic))
62
71
  values.push(transport.inspect(checkTopic));
63
72
  const value = { runId: init.runId, sequence: ++sequence, observedAt: new Date().toISOString(),
64
- sessions: values.map((item) => inspect(item, changedTopic)) };
73
+ sessions: values.map((item) => inspect(item, change)) };
65
74
  send({ ...envelope(), type: "snapshot", snapshot: value, ready: isReady });
66
75
  return value;
67
76
  }
@@ -78,9 +87,9 @@ export function runWalletSdkChild(createTransport) {
78
87
  stage = "restoration_failed";
79
88
  send({ ...envelope(), type: "stage", stage: "session_restore" });
80
89
  await transport.restore();
81
- transport.onSessionChanged((topic, selectionChanged) => {
90
+ transport.onSessionChanged((topic, chains) => {
82
91
  try {
83
- snapshot(false, selectionChanged ? topic : undefined);
92
+ snapshot(false, chains ? { topic, chains } : undefined);
84
93
  }
85
94
  catch {
86
95
  fail("wallet_state_unavailable");
@@ -97,7 +106,7 @@ export function runWalletSdkChild(createTransport) {
97
106
  throw new Error("Wallet service is not ready.");
98
107
  switch (input.type) {
99
108
  case "connect": {
100
- const pairing = await transport.connect();
109
+ const pairing = await transport.connect(input.chains);
101
110
  send({ ...reply, type: "pairing", uri: pairing.uri, expiresAt: pairing.expiresAt });
102
111
  const session = await pairing.approval;
103
112
  snapshot(false);
@@ -117,14 +126,22 @@ export function runWalletSdkChild(createTransport) {
117
126
  case "sign": {
118
127
  snapshot(false, undefined, input.topic);
119
128
  const current = sessions.get(input.topic);
120
- if (current.status !== "present" || current.version !== input.sessionVersion ||
121
- !current.session.accounts.includes(input.account) || !current.session.methods.includes("sui_signTransaction")) {
129
+ if (current.status !== "present" || current.lifecycleRevision !== input.lifecycleRevision ||
130
+ current.grantRevisions[input.chainId] !== input.grantRevision ||
131
+ !walletGrantAccount(current.session.grants, input.chainId, input.account)?.methods.includes("sui_signTransaction")) {
122
132
  send({ ...reply, type: "error", code: "session_changed" });
123
133
  return;
124
134
  }
125
135
  const result = await transport.sign(input);
126
136
  snapshot(false, undefined, input.topic);
127
- send({ ...reply, type: "signed", ...result, sessionVersion: sessions.get(input.topic).version });
137
+ const after = sessions.get(input.topic);
138
+ if (after.status !== "present" || after.lifecycleRevision !== input.lifecycleRevision ||
139
+ after.grantRevisions[input.chainId] !== input.grantRevision) {
140
+ send({ ...reply, type: "error", code: "session_changed" });
141
+ return;
142
+ }
143
+ send({ ...reply, type: "signed", ...result, chainId: input.chainId,
144
+ lifecycleRevision: after.lifecycleRevision, grantRevision: after.grantRevisions[input.chainId] });
128
145
  return;
129
146
  }
130
147
  }
@@ -1,17 +1,18 @@
1
1
  import { SERVER_VERSION } from "../mcp/serverInfo.js";
2
2
  import { z } from "zod";
3
3
  import { suiAddressStringSchema } from "../core/suiAddress.js";
4
- import { walletRunIdSchema, walletStartStageSchema, walletSnapshotSchema, walletSessionObservationSchema, walletSessionSchema } from "../core/session/walletRuntime.js";
4
+ import { walletRunIdSchema, walletStartStageSchema, walletSnapshotSchema, walletSessionObservationSchema, walletSessionSchema, suiWalletRevisionSchema } from "../core/session/walletRuntime.js";
5
+ import { walletChainsSchema } from "../core/session/chainAccounts.js";
5
6
  import { walletUnavailableReasonSchema } from "../core/session/walletConnection.js";
6
7
  const envelope = z.object({ packageVersion: z.literal(SERVER_VERSION), runId: walletRunIdSchema });
7
8
  const request = envelope.extend({ requestId: z.number().int().positive(), operationId: z.string().min(1) });
8
9
  export const walletCommandSchema = z.union([
9
10
  envelope.extend({ type: z.literal("init"), dataDirectory: z.string().min(1), projectId: z.string().regex(/^[0-9a-f]{32}$/i),
10
11
  metadata: z.object({ name: z.string(), description: z.string(), url: z.string() }).strict() }).strict(),
11
- request.extend({ type: z.literal("connect") }).strict(),
12
+ request.extend({ type: z.literal("connect"), chains: walletChainsSchema }).strict(),
12
13
  request.extend({ type: z.enum(["disconnect", "check_session"]), topic: z.string().min(1) }).strict(),
13
14
  request.extend({ type: z.literal("sign"), topic: z.string().min(1), account: suiAddressStringSchema,
14
- transactionBytesBase64: z.string().min(1), sessionVersion: z.number().int().positive() }).strict()
15
+ transactionBytesBase64: z.string().min(1), ...suiWalletRevisionSchema.shape }).strict()
15
16
  ]);
16
17
  const reply = envelope.extend({ requestId: z.number().int().positive(), operationId: z.string().min(1) });
17
18
  export const walletEventSchema = z.union([
@@ -22,6 +23,6 @@ export const walletEventSchema = z.union([
22
23
  reply.extend({ type: z.literal("connected"), session: walletSessionSchema }).strict(),
23
24
  reply.extend({ type: z.literal("disconnected") }).strict(),
24
25
  reply.extend({ type: z.literal("checked"), observation: walletSessionObservationSchema, sequence: z.number().int().positive() }).strict(),
25
- reply.extend({ type: z.literal("signed"), transactionBytes: z.string().min(1), signature: z.string().min(1), sessionVersion: z.number().int().positive() }).strict(),
26
+ reply.extend({ type: z.literal("signed"), transactionBytes: z.string().min(1), signature: z.string().min(1), ...suiWalletRevisionSchema.shape }).strict(),
26
27
  reply.extend({ type: z.literal("error"), code: z.enum(["rejected", "session_changed", "operation_failed"]) }).strict()
27
28
  ]);
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { walletUnavailable, WalletUnavailableError, WalletUserRejectedError } from "../core/session/walletConnection.js";
6
6
  import { WalletRunInterruptedError, WalletReplacementFailedError } from "../core/session/walletRuntime.js";
7
+ import { WALLET_CHAINS, comparableGrantScope } from "../core/session/chainAccounts.js";
7
8
  import { walletCommandSchema, walletEventSchema } from "./walletSdkIpc.js";
8
9
  export class WalletSdkProcess {
9
10
  options;
@@ -137,11 +138,19 @@ export class WalletSdkProcess {
137
138
  }
138
139
  topics.add(session.topic);
139
140
  const prior = run.snapshot?.sessions.find((entry) => entry.topic === session.topic);
140
- if (prior && (session.version < prior.version || session.version === prior.version && !isDeepStrictEqual(session, prior))) {
141
+ if (prior && (session.lifecycleRevision < prior.lifecycleRevision ||
142
+ session.lifecycleRevision === prior.lifecycleRevision && session.status !== prior.status || WALLET_CHAINS.some((chain) => session.grantRevisions[chain] < prior.grantRevisions[chain] ||
143
+ session.lifecycleRevision !== prior.lifecycleRevision && session.grantRevisions[chain] === prior.grantRevisions[chain] ||
144
+ session.grantRevisions[chain] === prior.grantRevisions[chain] &&
145
+ !isDeepStrictEqual(comparableGrantScope(session.status === "present" ? session.session.grants.find((grant) => grant.chainId === chain) : undefined), comparableGrantScope(prior.status === "present" ? prior.session.grants.find((grant) => grant.chainId === chain) : undefined))))) {
141
146
  this.lose(run, "wallet_state_unavailable");
142
147
  return;
143
148
  }
144
149
  }
150
+ if (run.snapshot?.sessions.some((session) => !topics.has(session.topic))) {
151
+ this.lose(run, "wallet_state_unavailable");
152
+ return;
153
+ }
145
154
  const previous = run.snapshot;
146
155
  run.snapshot = event.snapshot;
147
156
  if (event.ready) {
@@ -304,7 +313,7 @@ export class WalletSdkProcess {
304
313
  if (run !== this.run || run.fenced || this.closed)
305
314
  throw new WalletRunInterruptedError(run.id);
306
315
  if (this.state.status !== "available")
307
- throw new WalletUnavailableError("wallet_state_unavailable", this.state.message);
316
+ throw new WalletUnavailableError(this.state);
308
317
  };
309
318
  assertCurrent();
310
319
  const call = (input, operationId, approval) => {
@@ -322,32 +331,39 @@ export class WalletSdkProcess {
322
331
  });
323
332
  };
324
333
  return { runId: run.id, assertCurrent,
325
- connect: async (operationId) => {
334
+ connect: async (chains, operationId) => {
326
335
  let approve, rejectApproval;
327
336
  const approval = new Promise((resolve, reject) => { approve = resolve; rejectApproval = reject; });
328
337
  void approval.catch(() => { });
329
- const reply = await call({ type: "connect" }, operationId, { approve, rejectApproval });
338
+ const reply = await call({ type: "connect", chains: [...chains] }, operationId, { approve, rejectApproval });
330
339
  assertCurrent();
331
340
  if (reply.type !== "pairing")
332
341
  throw new Error("Wallet pairing response is unavailable.");
333
342
  return { uri: reply.uri, expiresAt: reply.expiresAt, approval };
334
343
  },
335
344
  disconnect: async (topic, operationId) => { await call({ type: "disconnect", topic }, operationId); assertCurrent(); },
336
- checkSession: async (topic, operationId) => {
345
+ checkSession: async (topic, operationId, chainId) => {
337
346
  const reply = await call({ type: "check_session", topic }, operationId);
338
347
  assertCurrent();
348
+ const current = run.snapshot?.sessions.find((item) => item.topic === topic);
339
349
  if (reply.type !== "checked" || reply.observation.topic !== topic || !run.snapshot || run.snapshot.sequence < reply.sequence ||
340
- run.snapshot.sessions.find((item) => item.topic === topic)?.version !== reply.observation.version) {
350
+ !current || current.lifecycleRevision !== reply.observation.lifecycleRevision ||
351
+ (chainId ? current.grantRevisions[chainId] !== reply.observation.grantRevisions[chainId]
352
+ : !isDeepStrictEqual(current.grantRevisions, reply.observation.grantRevisions))) {
341
353
  throw new Error("The wallet session changed during confirmation.");
342
354
  }
343
- return reply.observation;
355
+ return current;
344
356
  },
345
357
  sign: async (input, operationId) => {
346
358
  const reply = await call({ ...input, type: "sign" }, operationId);
347
359
  assertCurrent();
348
360
  if (reply.type !== "signed")
349
361
  throw new Error("The wallet signature response is unavailable.");
350
- return { transactionBytes: reply.transactionBytes, signature: reply.signature, sessionVersion: reply.sessionVersion };
362
+ if (reply.chainId !== input.chainId || reply.lifecycleRevision !== input.lifecycleRevision || reply.grantRevision !== input.grantRevision) {
363
+ throw new Error("The signed response belongs to a changed wallet grant.");
364
+ }
365
+ return { transactionBytes: reply.transactionBytes, signature: reply.signature, chainId: reply.chainId,
366
+ lifecycleRevision: reply.lifecycleRevision, grantRevision: reply.grantRevision };
351
367
  } };
352
368
  }
353
369
  }