@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/dist/mcp-ui/contracts.js
CHANGED
|
@@ -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()
|
|
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 = {
|
package/dist/mcp-ui/tools.js
CHANGED
|
@@ -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:
|
|
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()
|
|
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 = {
|
|
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 });
|
|
@@ -17,7 +17,7 @@ import { verifySuiChainReceipt } from "../core/action/suiChainReceiptVerifier.js
|
|
|
17
17
|
import { readPublicChainReceipt } from "../core/action/suiChainReceiptReader.js";
|
|
18
18
|
import { LocalSessionStore } from "../core/session/sessionStore.js";
|
|
19
19
|
import { createMcpServer } from "../mcp/server.js";
|
|
20
|
-
import { SERVER_NAME, SERVER_NETWORK, SERVER_VERSION } from "../mcp/serverInfo.js";
|
|
20
|
+
import { PRODUCT_DISPLAY_NAME, SERVER_NAME, SERVER_NETWORK, SERVER_VERSION } from "../mcp/serverInfo.js";
|
|
21
21
|
import { createReviewRequestHandler } from "../review-server/server.js";
|
|
22
22
|
import { DEFAULT_SUI_GRAPHQL_URL, DEFAULT_SUI_GRPC_URL, composeRuntimeConfig } from "./config.js";
|
|
23
23
|
import { DeepbookOfficialIndexerSource } from "../core/read/deepbookOfficialIndexerSource.js";
|
|
@@ -130,7 +130,7 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
|
|
|
130
130
|
logger.error("WalletConnect initialization unavailable", { stage: "initialization_failed" });
|
|
131
131
|
}
|
|
132
132
|
walletRuntime = new WalletSdkProcess({ projectId: WALLETCONNECT_PROJECT_ID, dataDirectory: dirname(bootConfig.activityDatabasePath),
|
|
133
|
-
...(metadata ? { metadata: { name:
|
|
133
|
+
...(metadata ? { metadata: { name: PRODUCT_DISPLAY_NAME, description: metadata.description, url: metadata.homepage } } : {}) });
|
|
134
134
|
workflow = new WalletWorkflow({ records: workflowRecords, sessions, ownerId,
|
|
135
135
|
runtime: walletRuntime, computation: reviewComputationDeps, verifyReceipt: chainReceiptVerifier, readReceipt: publicChainReceiptReader, signatureClient: suiClient,
|
|
136
136
|
assertCurrent: access.assertCurrent, bindExternalEvent: (work) => access.bind(work), logger,
|
|
@@ -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() };
|
package/dist/runtime/config.js
CHANGED
|
@@ -2,13 +2,10 @@ import { resolveActivityDatabasePath } from "../core/activity/sqliteActivityStor
|
|
|
2
2
|
import { DEFAULT_SUI_GRAPHQL_URL, DEFAULT_SUI_GRPC_URL, SUI_MAINNET_CHAIN_IDENTIFIER, parseGraphqlUrl, parseGrpcUrl } from "./suiEndpoint.js";
|
|
3
3
|
import { SETTINGS_APPLIES_AFTER_RESTART } from "../core/preferences/preferencesStore.js";
|
|
4
4
|
export function loadBootConfig(env = process.env) {
|
|
5
|
-
const network = env.
|
|
5
|
+
const network = env.SUI_MCP_NETWORK ?? "mainnet";
|
|
6
6
|
if (network !== "mainnet") {
|
|
7
7
|
throw new Error("Sui MCP product runtime only supports mainnet");
|
|
8
8
|
}
|
|
9
|
-
if (env.SUI_RPC_URL) {
|
|
10
|
-
throw new Error("Sui JSON-RPC endpoint configuration is not supported; use local settings, SUI_GRPC_URL for gRPC, or SUI_GRAPHQL_URL for GraphQL");
|
|
11
|
-
}
|
|
12
9
|
return {
|
|
13
10
|
network: "mainnet",
|
|
14
11
|
expectedChainIdentifier: SUI_MAINNET_CHAIN_IDENTIFIER,
|
|
@@ -34,8 +31,8 @@ export function composeRuntimeConfig(input) {
|
|
|
34
31
|
const defaultSuiGraphqlUrl = parseGraphqlUrl(input.defaultSuiGraphqlUrl ?? DEFAULT_SUI_GRAPHQL_URL);
|
|
35
32
|
const storedValue = parseGrpcUrl(input.storedSuiGrpcUrl ?? defaultSuiGrpcUrl);
|
|
36
33
|
const storedGraphqlValue = parseGraphqlUrl(input.storedSuiGraphqlUrl ?? defaultSuiGraphqlUrl);
|
|
37
|
-
const envValue = env.
|
|
38
|
-
const envGraphqlValue = env.
|
|
34
|
+
const envValue = env.SUI_MCP_GRPC_URL === undefined ? undefined : parseGrpcUrl(env.SUI_MCP_GRPC_URL);
|
|
35
|
+
const envGraphqlValue = env.SUI_MCP_GRAPHQL_URL === undefined ? undefined : parseGraphqlUrl(env.SUI_MCP_GRAPHQL_URL);
|
|
39
36
|
const suiGrpcUrl = resolveSuiGrpcUrlView({
|
|
40
37
|
defaultSuiGrpcUrl,
|
|
41
38
|
storedValue,
|
|
@@ -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;
|
|
@@ -19,12 +20,12 @@ export class RuntimeLocalSettingsService {
|
|
|
19
20
|
this.bootSuiGrpcUrl = normalizeBootSuiGrpcUrlView(options.bootSuiGrpcUrl ??
|
|
20
21
|
resolveSuiGrpcUrlView({
|
|
21
22
|
defaultSuiGrpcUrl: this.defaultSuiGrpcUrl,
|
|
22
|
-
envValue: this.env.
|
|
23
|
+
envValue: this.env.SUI_MCP_GRPC_URL
|
|
23
24
|
}));
|
|
24
25
|
this.bootSuiGraphqlUrl = normalizeBootSuiGraphqlUrlView(options.bootSuiGraphqlUrl ??
|
|
25
26
|
resolveSuiGraphqlUrlView({
|
|
26
27
|
defaultSuiGraphqlUrl: this.defaultSuiGraphqlUrl,
|
|
27
|
-
envValue: this.env.
|
|
28
|
+
envValue: this.env.SUI_MCP_GRAPHQL_URL
|
|
28
29
|
}));
|
|
29
30
|
this.verifyGrpcEndpoint = options.verifyGrpcEndpoint ?? defaultGrpcEndpointVerifier;
|
|
30
31
|
this.verifyGraphqlEndpoint = options.verifyGraphqlEndpoint ?? defaultGraphqlEndpointVerifier;
|
|
@@ -33,28 +34,30 @@ 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
|
-
|
|
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,
|
|
48
|
-
envValue: this.env.
|
|
50
|
+
envValue: this.env.SUI_MCP_GRPC_URL
|
|
49
51
|
});
|
|
50
52
|
const currentStoredGraphqlView = resolveSuiGraphqlUrlView({
|
|
51
53
|
defaultSuiGraphqlUrl: this.defaultSuiGraphqlUrl,
|
|
52
54
|
storedValue: storedGraphql,
|
|
53
|
-
envValue: this.env.
|
|
55
|
+
envValue: this.env.SUI_MCP_GRAPHQL_URL
|
|
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) {
|
|
@@ -3,8 +3,8 @@ import { link, lstat, mkdir, readFile, realpath, unlink, writeFile } from "node:
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { SERVER_VERSION } from "../../mcp/serverInfo.js";
|
|
6
|
-
export const IDENTITY_PATH = "/
|
|
7
|
-
export const INTERNAL_MCP_PATH = "/
|
|
6
|
+
export const IDENTITY_PATH = "/runtime/identity";
|
|
7
|
+
export const INTERNAL_MCP_PATH = "/runtime/mcp";
|
|
8
8
|
export const IDENTITY_CHALLENGE_HEADER = "x-sui-mcp-identity-challenge";
|
|
9
9
|
export const SERVER_INSTANCE_HEADER = "x-sui-mcp-server-instance";
|
|
10
10
|
export const IDENTITY_TIMEOUT_MS = 1000;
|
|
@@ -22,7 +22,7 @@ import { SmokeResponseShapeError, assertSmokeOkStatus, summarizeActivitySmokePay
|
|
|
22
22
|
import { SuiEndpointError, verifyMainnetGraphqlEndpoint, verifyMainnetGrpcEndpoint } from "./suiEndpoint.js";
|
|
23
23
|
import { GraphqlSuiTransactionActivitySource } from "./suiTransactionGraphqlSource.js";
|
|
24
24
|
const startedAt = new Date();
|
|
25
|
-
const smokeFunctionTarget = process.env.
|
|
25
|
+
const smokeFunctionTarget = process.env.SUI_MCP_SMOKE_FUNCTION_TARGET;
|
|
26
26
|
const plannedTools = [
|
|
27
27
|
TOOL_NAMES.readSummarizeWalletAssets,
|
|
28
28
|
TOOL_NAMES.readInspectDeepbookOrderbook,
|
|
@@ -65,11 +65,11 @@ class SmokeMainnetError extends Error {
|
|
|
65
65
|
async function main() {
|
|
66
66
|
const logger = createStderrLogger("smoke-mainnet");
|
|
67
67
|
const bootConfig = loadSmokeBootConfig();
|
|
68
|
-
const smokeAddress = parseSuiAddress(requiredEnv("
|
|
69
|
-
const poolKey = requiredEnv("
|
|
70
|
-
const quoteAmount = requiredEnv("
|
|
71
|
-
const inspectDigest = process.env.
|
|
72
|
-
const inspectRandomLatest = truthyEnv("
|
|
68
|
+
const smokeAddress = parseSuiAddress(requiredEnv("SUI_MCP_SMOKE_ADDRESS")) ?? fail("config_error", "SUI_MCP_SMOKE_ADDRESS must be a Sui address.");
|
|
69
|
+
const poolKey = requiredEnv("SUI_MCP_SMOKE_DEEPBOOK_POOL_KEY");
|
|
70
|
+
const quoteAmount = requiredEnv("SUI_MCP_SMOKE_QUOTE_AMOUNT");
|
|
71
|
+
const inspectDigest = process.env.SUI_MCP_SMOKE_INSPECT_DIGEST;
|
|
72
|
+
const inspectRandomLatest = truthyEnv("SUI_MCP_SMOKE_INSPECT_RANDOM_LATEST");
|
|
73
73
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
74
74
|
const client = new Client({ name: "sui-mcp-smoke", version: "0.0.0" });
|
|
75
75
|
let smokeDbDir;
|
|
@@ -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.
|
|
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: "
|
|
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: "
|
|
202
|
+
expectedAccountSource: "explicit_filter",
|
|
202
203
|
expectedRelationship: "sent",
|
|
203
204
|
requireFunction: true,
|
|
204
205
|
requireAnalysis: true,
|
|
@@ -329,18 +330,18 @@ function fail(category, message) {
|
|
|
329
330
|
}
|
|
330
331
|
function smokeEnvPresence() {
|
|
331
332
|
return {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
333
|
+
SUI_MCP_GRPC_URL: process.env.SUI_MCP_GRPC_URL !== undefined,
|
|
334
|
+
SUI_MCP_GRAPHQL_URL: process.env.SUI_MCP_GRAPHQL_URL !== undefined,
|
|
335
|
+
SUI_MCP_SMOKE_ADDRESS: process.env.SUI_MCP_SMOKE_ADDRESS !== undefined,
|
|
336
|
+
SUI_MCP_SMOKE_DEEPBOOK_POOL_KEY: process.env.SUI_MCP_SMOKE_DEEPBOOK_POOL_KEY !== undefined,
|
|
337
|
+
SUI_MCP_SMOKE_QUOTE_AMOUNT: process.env.SUI_MCP_SMOKE_QUOTE_AMOUNT !== undefined,
|
|
338
|
+
SUI_MCP_SMOKE_INSPECT_DIGEST: process.env.SUI_MCP_SMOKE_INSPECT_DIGEST !== undefined,
|
|
339
|
+
SUI_MCP_SMOKE_INSPECT_RANDOM_LATEST: process.env.SUI_MCP_SMOKE_INSPECT_RANDOM_LATEST !== undefined,
|
|
340
|
+
SUI_MCP_SMOKE_FUNCTION_TARGET: process.env.SUI_MCP_SMOKE_FUNCTION_TARGET !== undefined,
|
|
341
|
+
SUI_MCP_SMOKE_SWAP_PROTOCOL: process.env.SUI_MCP_SMOKE_SWAP_PROTOCOL !== undefined,
|
|
342
|
+
SUI_MCP_SMOKE_SWAP_FROM_SYMBOL: process.env.SUI_MCP_SMOKE_SWAP_FROM_SYMBOL !== undefined,
|
|
343
|
+
SUI_MCP_SMOKE_SWAP_TO_SYMBOL: process.env.SUI_MCP_SMOKE_SWAP_TO_SYMBOL !== undefined,
|
|
344
|
+
SUI_MCP_SMOKE_SWAP_AMOUNT_DISPLAY: process.env.SUI_MCP_SMOKE_SWAP_AMOUNT_DISPLAY !== undefined
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
347
|
function tryWriteSmokeResult(record) {
|
|
@@ -2,27 +2,27 @@ import { INTENT_PLAN_FACTORIES, resolveIntentPlanFactory, swapIntentInputSchema
|
|
|
2
2
|
import { computeReviewStateWithPrivateArtifacts } from "../core/review/reviewComputation.js";
|
|
3
3
|
export class SmokeReviewConfigError extends Error {
|
|
4
4
|
}
|
|
5
|
-
const requiredSwapInputs = ["
|
|
5
|
+
const requiredSwapInputs = ["SUI_MCP_SMOKE_SWAP_PROTOCOL", "SUI_MCP_SMOKE_SWAP_FROM_SYMBOL", "SUI_MCP_SMOKE_SWAP_TO_SYMBOL", "SUI_MCP_SMOKE_SWAP_AMOUNT_DISPLAY"];
|
|
6
6
|
export async function computeSmokeReview(options) {
|
|
7
7
|
const { env, sessions, computation, account } = options;
|
|
8
|
-
const rawSlippage = env.
|
|
8
|
+
const rawSlippage = env.SUI_MCP_SMOKE_SWAP_MAX_SLIPPAGE_BPS;
|
|
9
9
|
if (rawSlippage !== undefined && (!Number.isInteger(Number(rawSlippage)) || Number(rawSlippage) < 1)) {
|
|
10
|
-
throw new SmokeReviewConfigError("
|
|
10
|
+
throw new SmokeReviewConfigError("SUI_MCP_SMOKE_SWAP_MAX_SLIPPAGE_BPS must be a positive integer.");
|
|
11
11
|
}
|
|
12
12
|
const missing = requiredSwapInputs.filter((name) => env[name] === undefined);
|
|
13
13
|
if (missing.length === requiredSwapInputs.length)
|
|
14
14
|
return { status: "not_run", notRunReason: "missing_env", requiredEnv: [...requiredSwapInputs] };
|
|
15
15
|
if (missing.length)
|
|
16
16
|
throw new SmokeReviewConfigError(`Partial account-bound swap smoke configuration; missing: ${missing.join(", ")}`);
|
|
17
|
-
const parsed = swapIntentInputSchema.safeParse({ type: "swap", protocol: env.
|
|
18
|
-
from: { symbol: env.
|
|
19
|
-
to: { symbol: env.
|
|
17
|
+
const parsed = swapIntentInputSchema.safeParse({ type: "swap", protocol: env.SUI_MCP_SMOKE_SWAP_PROTOCOL,
|
|
18
|
+
from: { symbol: env.SUI_MCP_SMOKE_SWAP_FROM_SYMBOL, amount: env.SUI_MCP_SMOKE_SWAP_AMOUNT_DISPLAY },
|
|
19
|
+
to: { symbol: env.SUI_MCP_SMOKE_SWAP_TO_SYMBOL }, maxSlippageBps: Number(env.SUI_MCP_SMOKE_SWAP_MAX_SLIPPAGE_BPS ?? "50") });
|
|
20
20
|
if (!parsed.success)
|
|
21
21
|
throw new SmokeReviewConfigError("The optional swap smoke inputs are invalid.");
|
|
22
22
|
const { protocol, ...intent } = parsed.data;
|
|
23
23
|
const resolution = resolveIntentPlanFactory(INTENT_PLAN_FACTORIES, "swap", protocol);
|
|
24
24
|
if (resolution.status !== "resolved")
|
|
25
|
-
throw new SmokeReviewConfigError("
|
|
25
|
+
throw new SmokeReviewConfigError("SUI_MCP_SMOKE_SWAP_PROTOCOL must select a supported swap adapter.");
|
|
26
26
|
const now = options.now ?? new Date(), plan = resolution.factory.createPlan(intent, now);
|
|
27
27
|
const created = await sessions.createReviewSession([plan], now);
|
|
28
28
|
await sessions.recordWalletConnected(created.session.id, account, now);
|
package/dist/runtime/start.js
CHANGED
|
@@ -40,7 +40,7 @@ async function main() {
|
|
|
40
40
|
const controlReady = loadControlIdentity(bootConfig.activityDatabasePath, {
|
|
41
41
|
network: bootConfig.network, chainIdentifier: bootConfig.expectedChainIdentifier,
|
|
42
42
|
walletConnectProjectId: WALLETCONNECT_PROJECT_ID,
|
|
43
|
-
grpcOverride: process.env.
|
|
43
|
+
grpcOverride: process.env.SUI_MCP_GRPC_URL ?? null, graphqlOverride: process.env.SUI_MCP_GRAPHQL_URL ?? null
|
|
44
44
|
});
|
|
45
45
|
void controlReady.catch(() => { });
|
|
46
46
|
stage = "stdio_start";
|
|
@@ -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 {
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
.
|
|
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,
|
|
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
|
|
82
|
-
|
|
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
|
|
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,
|
|
46
|
+
const inspect = (value, change) => {
|
|
46
47
|
const prior = sessions.get(value.topic);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
90
|
+
transport.onSessionChanged((topic, chains) => {
|
|
82
91
|
try {
|
|
83
|
-
snapshot(false,
|
|
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.
|
|
121
|
-
|
|
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
|
-
|
|
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),
|
|
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),
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|