@zktx.io/sui-mcp 0.5.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 (218) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/@mysten-bcs-2.0.5-Apache-2.0.txt +205 -0
  3. package/LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt +201 -0
  4. package/LICENSES/@mysten-utils-0.3.3-Apache-2.0.txt +205 -0
  5. package/LICENSES/fancy-canvas-2.1.0-MIT.txt +7 -0
  6. package/LICENSES/lightweight-charts-5.2.0-NOTICE.txt +2 -0
  7. package/README.md +288 -0
  8. package/dist/adapters/adapterLifecycleValidators.js +5 -0
  9. package/dist/adapters/adapterPromptSurfaces.js +60 -0
  10. package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +178 -0
  11. package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
  12. package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
  13. package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
  14. package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
  15. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +254 -0
  16. package/dist/adapters/intentPlanFactories.js +51 -0
  17. package/dist/adapters/reviewAdapters.js +47 -0
  18. package/dist/core/action/adapterLifecycleValidation.js +12 -0
  19. package/dist/core/action/contractNameRegistry.js +151 -0
  20. package/dist/core/action/forbiddenFields.js +43 -0
  21. package/dist/core/action/humanReadableReviewEvidence.js +193 -0
  22. package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
  23. package/dist/core/action/ptbVisualizationProducer.js +71 -0
  24. package/dist/core/action/receiptPtbGraph.js +29 -0
  25. package/dist/core/action/reviewCheckResults.js +6 -0
  26. package/dist/core/action/reviewStateValidation.js +11 -0
  27. package/dist/core/action/reviewTimeSimulationEvidence.js +470 -0
  28. package/dist/core/action/schemas.js +491 -0
  29. package/dist/core/action/signableAdapterContract.js +998 -0
  30. package/dist/core/action/suiChainReceiptEvidence.js +106 -0
  31. package/dist/core/action/suiChainReceiptMapping.js +107 -0
  32. package/dist/core/action/suiChainReceiptReader.js +211 -0
  33. package/dist/core/action/suiChainReceiptVerifier.js +178 -0
  34. package/dist/core/action/swapHumanReadableReviewProjection.js +130 -0
  35. package/dist/core/action/swapQuotePolicyEvidence.js +271 -0
  36. package/dist/core/action/transactionFunding.js +244 -0
  37. package/dist/core/action/transactionObjectOwnershipEvidence.js +256 -0
  38. package/dist/core/action/transactionObjectOwnershipProducer.js +323 -0
  39. package/dist/core/action/types.js +33 -0
  40. package/dist/core/action/walletReviewContractAssembler.js +286 -0
  41. package/dist/core/activity/accountAssetTimeline.js +157 -0
  42. package/dist/core/activity/accountAssetTimelineUsdcReferences.js +120 -0
  43. package/dist/core/activity/activityStore.js +16 -0
  44. package/dist/core/activity/externalActivityCoverage.js +76 -0
  45. package/dist/core/activity/externalActivityTransactionStream.js +82 -0
  46. package/dist/core/activity/localDataService.js +296 -0
  47. package/dist/core/activity/localDataTypes.js +11 -0
  48. package/dist/core/activity/localDataValidation.js +439 -0
  49. package/dist/core/activity/schemaVersion.js +2 -0
  50. package/dist/core/activity/sqliteActivityStore.js +1068 -0
  51. package/dist/core/activity/sqliteActivityStoreRows.js +389 -0
  52. package/dist/core/activity/sqliteActivityStoreSchema.js +367 -0
  53. package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
  54. package/dist/core/activity/suiFunctionTarget.js +43 -0
  55. package/dist/core/activity/transactionActivityAccountEffects.js +186 -0
  56. package/dist/core/activity/transactionActivityAnalysis.js +295 -0
  57. package/dist/core/activity/transactionActivityClassifier.js +306 -0
  58. package/dist/core/activity/transactionActivityDetails.js +229 -0
  59. package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
  60. package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
  61. package/dist/core/activity/transactionActivityService.js +379 -0
  62. package/dist/core/activity/transactionActivityTypes.js +18 -0
  63. package/dist/core/eventlog/sink.js +35 -0
  64. package/dist/core/evidence/settlementFamilies.js +85 -0
  65. package/dist/core/evidence/userAnswerUse.js +1 -0
  66. package/dist/core/numeric/rawU64.js +63 -0
  67. package/dist/core/preferences/preferencesStore.js +26 -0
  68. package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
  69. package/dist/core/proposal/externalProposalReview.js +347 -0
  70. package/dist/core/proposal/schemas.js +208 -0
  71. package/dist/core/proposal/types.js +35 -0
  72. package/dist/core/read/amounts.js +14 -0
  73. package/dist/core/read/coinMetadata.js +60 -0
  74. package/dist/core/read/deepbookOfficialIndexerSource.js +373 -0
  75. package/dist/core/read/deepbookRawQuoteClient.js +86 -0
  76. package/dist/core/read/deepbookReadHelpers.js +321 -0
  77. package/dist/core/read/deepbookRegistry.js +157 -0
  78. package/dist/core/read/deepbookSourceOwners.js +159 -0
  79. package/dist/core/read/deepbookUsdcChartService.js +342 -0
  80. package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
  81. package/dist/core/read/readCardInputs.js +19 -0
  82. package/dist/core/read/readCardResult.js +40 -0
  83. package/dist/core/read/readResponseGuidance.js +402 -0
  84. package/dist/core/read/readService.js +1639 -0
  85. package/dist/core/read/readServiceTypes.js +88 -0
  86. package/dist/core/read/settlementParityFormatting.js +82 -0
  87. package/dist/core/read/walletReadHelpers.js +99 -0
  88. package/dist/core/review/reviewChecks.js +45 -0
  89. package/dist/core/review/reviewComputation.js +38 -0
  90. package/dist/core/review/reviewComputationResult.js +87 -0
  91. package/dist/core/session/cardSession.js +31 -0
  92. package/dist/core/session/cardSessionStore.js +204 -0
  93. package/dist/core/session/keyedRecordStore.js +18 -0
  94. package/dist/core/session/liveReviewSessionContract.js +3 -0
  95. package/dist/core/session/localSession.js +31 -0
  96. package/dist/core/session/privateReviewArtifacts.js +75 -0
  97. package/dist/core/session/reviewValidity.js +49 -0
  98. package/dist/core/session/sessionErrors.js +9 -0
  99. package/dist/core/session/sessionRecordStore.js +40 -0
  100. package/dist/core/session/sessionStore.js +655 -0
  101. package/dist/core/session/settingsSession.js +1 -0
  102. package/dist/core/session/settingsSessions.js +45 -0
  103. package/dist/core/session/sqliteCardStore.js +88 -0
  104. package/dist/core/session/sqliteSessionStore.js +213 -0
  105. package/dist/core/session/sqliteTransactionMaterialStore.js +64 -0
  106. package/dist/core/session/sqliteWalletWorkflowStore.js +821 -0
  107. package/dist/core/session/status.js +60 -0
  108. package/dist/core/session/transactionMaterialStore.js +221 -0
  109. package/dist/core/session/transactionRequest.js +72 -0
  110. package/dist/core/session/wait.js +79 -0
  111. package/dist/core/session/walletConnection.js +92 -0
  112. package/dist/core/session/walletRuntime.js +38 -0
  113. package/dist/core/session/walletWorkflow.js +608 -0
  114. package/dist/core/session/workflowState.js +80 -0
  115. package/dist/core/session/workflowView.js +150 -0
  116. package/dist/core/suiAddress.js +18 -0
  117. package/dist/core/suiEndpoint.js +72 -0
  118. package/dist/mcp/activeAccountResponse.js +24 -0
  119. package/dist/mcp/prompts.js +146 -0
  120. package/dist/mcp/registerTool.js +19 -0
  121. package/dist/mcp/resources.js +72 -0
  122. package/dist/mcp/responseGuidance.js +326 -0
  123. package/dist/mcp/result.js +17 -0
  124. package/dist/mcp/schemas.js +11 -0
  125. package/dist/mcp/server.js +32 -0
  126. package/dist/mcp/serverInfo.js +131 -0
  127. package/dist/mcp/toolErrors.js +118 -0
  128. package/dist/mcp/toolNames.js +58 -0
  129. package/dist/mcp/tools/account/index.js +44 -0
  130. package/dist/mcp/tools/action/prepareSuiActionReview.js +126 -0
  131. package/dist/mcp/tools/read/commonSchemas.js +43 -0
  132. package/dist/mcp/tools/read/deepbookReadTools.js +686 -0
  133. package/dist/mcp/tools/read/index.js +14 -0
  134. package/dist/mcp/tools/read/readToolHelpers.js +97 -0
  135. package/dist/mcp/tools/read/reviewActivityTools.js +179 -0
  136. package/dist/mcp/tools/read/serverStatusTools.js +100 -0
  137. package/dist/mcp/tools/read/transactionActivityOutput.js +371 -0
  138. package/dist/mcp/tools/read/transactionActivityTools.js +858 -0
  139. package/dist/mcp/tools/read/walletReadTools.js +734 -0
  140. package/dist/mcp/tools/session/executionResultTools.js +44 -0
  141. package/dist/mcp/tools/session/index.js +8 -0
  142. package/dist/mcp/tools/session/shared.js +36 -0
  143. package/dist/mcp/tools/session/statusTools.js +69 -0
  144. package/dist/mcp/tools/session/walletConnectionTools.js +69 -0
  145. package/dist/mcp/tools/settings/index.js +66 -0
  146. package/dist/mcp-app/account.html +3133 -0
  147. package/dist/mcp-app/account.notices.txt +973 -0
  148. package/dist/mcp-app/chart.html +3354 -0
  149. package/dist/mcp-app/chart.notices.txt +1194 -0
  150. package/dist/mcp-app/connect.html +3179 -0
  151. package/dist/mcp-app/connect.notices.txt +1012 -0
  152. package/dist/mcp-app/receipt.html +8430 -0
  153. package/dist/mcp-app/receipt.notices.txt +3549 -0
  154. package/dist/mcp-app/review.html +8430 -0
  155. package/dist/mcp-app/review.notices.txt +3549 -0
  156. package/dist/mcp-ui/contracts.js +31 -0
  157. package/dist/mcp-ui/readCards.js +28 -0
  158. package/dist/mcp-ui/tools.js +134 -0
  159. package/dist/review-app/brand-dark.svg +1 -0
  160. package/dist/review-app/brand-light.svg +1 -0
  161. package/dist/review-app/favicon.svg +1 -0
  162. package/dist/review-app/settings.css +1 -0
  163. package/dist/review-app/settings.js +1 -0
  164. package/dist/review-app/ui.css +809 -0
  165. package/dist/review-server/assets.js +48 -0
  166. package/dist/review-server/html.js +45 -0
  167. package/dist/review-server/http.js +47 -0
  168. package/dist/review-server/middleware/hostOrigin.js +48 -0
  169. package/dist/review-server/middleware/reviewToken.js +7 -0
  170. package/dist/review-server/reviewServerPolicy.js +1 -0
  171. package/dist/review-server/server.js +72 -0
  172. package/dist/review-server/settingsApi.js +158 -0
  173. package/dist/runtime/application.js +203 -0
  174. package/dist/runtime/config.js +97 -0
  175. package/dist/runtime/localSettingsService.js +198 -0
  176. package/dist/runtime/logger.js +66 -0
  177. package/dist/runtime/reviewDependencies.js +58 -0
  178. package/dist/runtime/reviewServerAcquire.js +93 -0
  179. package/dist/runtime/runtimeTermination.js +60 -0
  180. package/dist/runtime/shared/authenticatedFetch.js +127 -0
  181. package/dist/runtime/shared/control.js +93 -0
  182. package/dist/runtime/shared/dataAccess.js +66 -0
  183. package/dist/runtime/shared/mcpHttp.js +84 -0
  184. package/dist/runtime/shared/ownerLease.js +50 -0
  185. package/dist/runtime/shared/server.js +98 -0
  186. package/dist/runtime/shared/stdio.js +113 -0
  187. package/dist/runtime/smokeMainnetRead.js +391 -0
  188. package/dist/runtime/smokeMainnetReadAssertions.js +306 -0
  189. package/dist/runtime/smokeReview.js +40 -0
  190. package/dist/runtime/start.js +90 -0
  191. package/dist/runtime/suiEndpoint.js +97 -0
  192. package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
  193. package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
  194. package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
  195. package/dist/runtime/walletConnectConfig.js +3 -0
  196. package/dist/runtime/walletConnectStorage.js +99 -0
  197. package/dist/runtime/walletConnectTransport.js +128 -0
  198. package/dist/runtime/walletSdkChild.js +3 -0
  199. package/dist/runtime/walletSdkChildRuntime.js +160 -0
  200. package/dist/runtime/walletSdkIpc.js +26 -0
  201. package/dist/runtime/walletSdkProcess.js +358 -0
  202. package/docs/AGENT_BEHAVIOR.md +524 -0
  203. package/docs/AGENT_DEVELOPMENT_POLICY.md +815 -0
  204. package/docs/FRONTEND_POLICY.md +534 -0
  205. package/docs/LOCAL_DB_ARCHITECTURE.md +381 -0
  206. package/docs/MCP_SETUP.md +742 -0
  207. package/docs/MCP_TOOLS.md +1318 -0
  208. package/docs/SDK_API.md +206 -0
  209. package/docs/SIGNABLE_ADAPTER_CONTRACT.md +396 -0
  210. package/docs/TRANSACTION_ACTIVITY_LOG.md +194 -0
  211. package/docs/UTILITY_INDEX.md +275 -0
  212. package/docs/WALLET_CONNECTION.md +238 -0
  213. package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +166 -0
  214. package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +161 -0
  215. package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +259 -0
  216. package/package.json +79 -0
  217. package/protocols/deepbook-margin.md +15 -0
  218. package/protocols/deepbook-v3.md +24 -0
@@ -0,0 +1,60 @@
1
+ import { isRequestWaiting } from "./transactionRequest.js";
2
+ import { workflowProgress } from "./walletConnection.js";
3
+ export function reviewProgress(preparing, request, availability, observation) {
4
+ if (preparing)
5
+ return workflowProgress(true, true, availability);
6
+ if (request?.requestStatus === "completed")
7
+ return { status: "idle" };
8
+ const waiting = !!request && !observation.stopped &&
9
+ (observation.pending || ["awaiting_signature", "submitting", "awaiting_chain_result"].includes(request.requestStatus));
10
+ return workflowProgress(waiting, request?.requestStatus === "awaiting_signature", availability);
11
+ }
12
+ export const EXECUTION_POLLING_INTERVAL_SECONDS = 3;
13
+ export const EXECUTION_STATUS_CATEGORIES = ["final", "user_action_required", "awaiting_chain_result", "non_terminal"];
14
+ export function isFinalSessionStatus(status) { return status === "expired"; }
15
+ export function getExecutionPollingStatus(session, request) {
16
+ return request?.requestStatus ?? session.status;
17
+ }
18
+ export function executionStatusCategory(status) {
19
+ if (["completed", "request_failed", "stopped", "outcome_unknown", "expired"].includes(status))
20
+ return "final";
21
+ if (status === "awaiting_chain_result")
22
+ return "awaiting_chain_result";
23
+ if (status === "awaiting_signature" || status === "submitting")
24
+ return "non_terminal";
25
+ return "user_action_required";
26
+ }
27
+ export function isWaitStoppingExecutionStatus(status) {
28
+ const category = executionStatusCategory(status);
29
+ return category === "final" || category === "user_action_required";
30
+ }
31
+ export function isInteractionPendingReviewStatus(status) {
32
+ return executionStatusCategory(status) !== "final";
33
+ }
34
+ export function isReviewInteractionPending({ hasReviewInput, request, progress }) {
35
+ // Historical preparation state is not proof that a live input remains.
36
+ // Consumed input, in turn, does not end its admitted request or observation.
37
+ return hasReviewInput || progress.status !== "idle" || !!request && isRequestWaiting(request.requestStatus);
38
+ }
39
+ export function executionPollingHint() {
40
+ return {
41
+ nonTerminalStatuses: ["awaiting_signature", "submitting", "awaiting_chain_result"],
42
+ waitStoppingStatuses: ["proposed", "awaiting_wallet", "wallet_connected", "ready_for_wallet_review", "refresh_required", "blocked", "expired",
43
+ "stopped", "request_failed", "outcome_unknown", "completed"],
44
+ finalStatuses: ["stopped", "request_failed", "outcome_unknown", "completed", "expired"],
45
+ userActionRequiredStatuses: ["proposed", "awaiting_wallet", "wallet_connected", "ready_for_wallet_review", "refresh_required", "blocked"],
46
+ recommendedIntervalSeconds: EXECUTION_POLLING_INTERVAL_SECONDS
47
+ };
48
+ }
49
+ export function reviewStatusResponse({ session, request, walletAvailability, walletObservation, progress, connectionConflict }) {
50
+ const pollingStatus = getExecutionPollingStatus(session, request);
51
+ return {
52
+ walletAvailability, walletObservation, progress, connectionConflict,
53
+ reviewSessionId: session.id, status: session.status, reviewRevision: session.reviewRevision,
54
+ account: session.account, plans: session.plans, reviewState: session.reviewState,
55
+ pollingStatus, statusCategory: executionStatusCategory(pollingStatus), pollingHint: executionPollingHint(),
56
+ expiresAt: session.expiresAt, lastActivityAt: session.lastActivityAt,
57
+ ...(request ? { attemptId: request.attemptId, requestStatus: request.requestStatus, request,
58
+ ...(request.execution ? { executionResult: request.execution } : {}) } : {})
59
+ };
60
+ }
@@ -0,0 +1,221 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Transaction } from "@mysten/sui/transactions";
3
+ import { z } from "zod";
4
+ import { normalizedSuiAddressSchema, parseSuiAddress, suiTransactionDigestSchema } from "../suiAddress.js";
5
+ export const LOCAL_TRANSACTION_MATERIAL_KINDS = [
6
+ "deepbook_swap_transaction_data"
7
+ ];
8
+ export const LOCAL_TRANSACTION_MATERIAL_SOURCES = [
9
+ "sui_mcp_built",
10
+ "sui_mcp_verified"
11
+ ];
12
+ const localTransactionMaterialKindSchema = z.enum(LOCAL_TRANSACTION_MATERIAL_KINDS);
13
+ const localTransactionMaterialSourceSchema = z.enum(LOCAL_TRANSACTION_MATERIAL_SOURCES);
14
+ const isoUtcStringSchema = z.string().refine((value) => {
15
+ const parsed = new Date(value);
16
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
17
+ }, "Expected ISO 8601 UTC timestamp");
18
+ export const localTransactionMaterialHandleSchema = z.object({
19
+ materialId: z.string().min(1),
20
+ reviewSessionId: z.string().min(1),
21
+ planId: z.string().min(1),
22
+ account: normalizedSuiAddressSchema,
23
+ kind: localTransactionMaterialKindSchema,
24
+ source: localTransactionMaterialSourceSchema,
25
+ createdAt: isoUtcStringSchema,
26
+ expiresAt: isoUtcStringSchema
27
+ }).superRefine((value, ctx) => {
28
+ if (Date.parse(value.expiresAt) <= Date.parse(value.createdAt)) {
29
+ ctx.addIssue({
30
+ code: "custom",
31
+ path: ["expiresAt"],
32
+ message: "expiresAt must be after createdAt"
33
+ });
34
+ }
35
+ });
36
+ export const localTransactionMaterialDigestCommitmentSchema = z.object({
37
+ materialId: z.string().min(1),
38
+ reviewSessionId: z.string().min(1),
39
+ planId: z.string().min(1),
40
+ account: normalizedSuiAddressSchema,
41
+ kind: localTransactionMaterialKindSchema,
42
+ source: localTransactionMaterialSourceSchema,
43
+ digestKind: z.literal("sui_transaction_digest"),
44
+ transactionDigest: suiTransactionDigestSchema,
45
+ computedAt: isoUtcStringSchema,
46
+ expiresAt: isoUtcStringSchema
47
+ });
48
+ export function parseLocalTransactionMaterialHandle(value) {
49
+ return localTransactionMaterialHandleSchema.parse(value);
50
+ }
51
+ export function parseLocalTransactionMaterialDigestCommitment(value) {
52
+ return localTransactionMaterialDigestCommitmentSchema.parse(value);
53
+ }
54
+ function parseLocalTransactionMaterialArtifacts(value, now = new Date()) {
55
+ const transactionMaterial = parseLocalTransactionMaterialHandle(value.transactionMaterial);
56
+ const transactionMaterialDigest = parseLocalTransactionMaterialDigestCommitment(value.transactionMaterialDigest);
57
+ const materialCreatedAtMs = Date.parse(transactionMaterial.createdAt);
58
+ const materialExpiresAtMs = Date.parse(transactionMaterial.expiresAt);
59
+ const digestComputedAtMs = Date.parse(transactionMaterialDigest.computedAt);
60
+ const nowMs = now.getTime();
61
+ if (!Number.isFinite(nowMs)) {
62
+ throw new LocalTransactionMaterialStoreError("now must be a valid Date");
63
+ }
64
+ if (materialCreatedAtMs > nowMs) {
65
+ throw new LocalTransactionMaterialStoreError("transaction material createdAt must not be in the future");
66
+ }
67
+ if (materialExpiresAtMs <= nowMs) {
68
+ throw new LocalTransactionMaterialStoreError("transaction material must not be expired");
69
+ }
70
+ if (digestComputedAtMs < materialCreatedAtMs || digestComputedAtMs > nowMs) {
71
+ throw new LocalTransactionMaterialStoreError("transaction material digest computedAt must be between createdAt and now");
72
+ }
73
+ if (digestComputedAtMs >= materialExpiresAtMs) {
74
+ throw new LocalTransactionMaterialStoreError("transaction material digest must be computed before material expiry");
75
+ }
76
+ if (transactionMaterialDigest.materialId !== transactionMaterial.materialId ||
77
+ transactionMaterialDigest.reviewSessionId !== transactionMaterial.reviewSessionId ||
78
+ transactionMaterialDigest.planId !== transactionMaterial.planId ||
79
+ transactionMaterialDigest.account !== transactionMaterial.account ||
80
+ transactionMaterialDigest.kind !== transactionMaterial.kind ||
81
+ transactionMaterialDigest.source !== transactionMaterial.source ||
82
+ transactionMaterialDigest.expiresAt !== transactionMaterial.expiresAt) {
83
+ throw new LocalTransactionMaterialStoreError("transaction material digest must match the material handle");
84
+ }
85
+ return { transactionMaterial, transactionMaterialDigest };
86
+ }
87
+ export async function verifyLocalTransactionMaterialArtifacts(value) {
88
+ const now = value.now ?? new Date();
89
+ const parsed = parseLocalTransactionMaterialArtifacts({
90
+ transactionMaterial: value.transactionMaterial,
91
+ transactionMaterialDigest: value.transactionMaterialDigest
92
+ }, now);
93
+ const material = value.materialStore.getTransactionMaterial(parsed.transactionMaterial, now);
94
+ if (!material) {
95
+ throw new LocalTransactionMaterialStoreError("transaction material is unavailable");
96
+ }
97
+ let transactionDigest;
98
+ try {
99
+ transactionDigest = await Transaction.from(material.transactionBytes).getDigest();
100
+ }
101
+ catch {
102
+ throw new LocalTransactionMaterialStoreError("transaction material bytes cannot produce a Sui transaction digest");
103
+ }
104
+ const parsedDigest = suiTransactionDigestSchema.safeParse(transactionDigest);
105
+ if (!parsedDigest.success) {
106
+ throw new LocalTransactionMaterialStoreError("computed transaction digest is invalid");
107
+ }
108
+ if (parsedDigest.data !== parsed.transactionMaterialDigest.transactionDigest) {
109
+ throw new LocalTransactionMaterialStoreError("transaction material digest does not match stored transaction bytes");
110
+ }
111
+ return { ...parsed, transactionBytes: new Uint8Array(material.transactionBytes) };
112
+ }
113
+ export class LocalTransactionMaterialStoreError extends Error {
114
+ constructor(message) {
115
+ super(message);
116
+ }
117
+ }
118
+ // Validate the store input and build the stored record (with cloned bytes). Shared
119
+ // by every LocalTransactionMaterialStore implementation so validation never drifts
120
+ // between the in-memory and SQLite backends.
121
+ export function buildTransactionMaterialRecord(input, now = new Date()) {
122
+ const account = parseSuiAddress(input.account);
123
+ if (!account) {
124
+ throw new LocalTransactionMaterialStoreError("Invalid transaction material account");
125
+ }
126
+ if (!input.reviewSessionId) {
127
+ throw new LocalTransactionMaterialStoreError("reviewSessionId is required");
128
+ }
129
+ if (!input.planId) {
130
+ throw new LocalTransactionMaterialStoreError("planId is required");
131
+ }
132
+ if (!LOCAL_TRANSACTION_MATERIAL_KINDS.includes(input.kind)) {
133
+ throw new LocalTransactionMaterialStoreError("Invalid transaction material kind");
134
+ }
135
+ if (!LOCAL_TRANSACTION_MATERIAL_SOURCES.includes(input.source)) {
136
+ throw new LocalTransactionMaterialStoreError("Invalid transaction material source");
137
+ }
138
+ if (input.transactionBytes.byteLength === 0) {
139
+ throw new LocalTransactionMaterialStoreError("transactionBytes must not be empty");
140
+ }
141
+ const createdAt = now.toISOString();
142
+ const expiresAt = input.expiresAt.toISOString();
143
+ if (Date.parse(expiresAt) <= Date.parse(createdAt)) {
144
+ throw new LocalTransactionMaterialStoreError("expiresAt must be after createdAt");
145
+ }
146
+ return {
147
+ materialId: `txmat_${randomUUID()}`,
148
+ reviewSessionId: input.reviewSessionId,
149
+ planId: input.planId,
150
+ account,
151
+ kind: input.kind,
152
+ source: input.source,
153
+ createdAt,
154
+ expiresAt,
155
+ transactionBytes: cloneBytes(input.transactionBytes),
156
+ ...(input.redactedDiagnostics === undefined
157
+ ? {}
158
+ : { redactedDiagnostics: structuredClone(input.redactedDiagnostics) })
159
+ };
160
+ }
161
+ // Project the public (redacted) handle out of a stored record.
162
+ export function toMaterialHandle(record) {
163
+ return {
164
+ materialId: record.materialId,
165
+ reviewSessionId: record.reviewSessionId,
166
+ planId: record.planId,
167
+ account: record.account,
168
+ kind: record.kind,
169
+ source: record.source,
170
+ createdAt: record.createdAt,
171
+ expiresAt: record.expiresAt
172
+ };
173
+ }
174
+ export class InMemoryLocalTransactionMaterialStore {
175
+ records = new Map();
176
+ recordTransactionMaterial(input, now = new Date()) {
177
+ const record = buildTransactionMaterialRecord(input, now);
178
+ this.records.set(record.materialId, record);
179
+ return toMaterialHandle(record);
180
+ }
181
+ getTransactionMaterial(handle, now = new Date()) {
182
+ const record = this.records.get(handle.materialId);
183
+ if (!record) {
184
+ return undefined;
185
+ }
186
+ if (Date.parse(record.expiresAt) <= now.getTime()) {
187
+ this.records.delete(handle.materialId);
188
+ return undefined;
189
+ }
190
+ if (!sameHandle(record, handle)) {
191
+ return undefined;
192
+ }
193
+ return {
194
+ ...record,
195
+ transactionBytes: cloneBytes(record.transactionBytes),
196
+ ...(record.redactedDiagnostics === undefined
197
+ ? {}
198
+ : { redactedDiagnostics: structuredClone(record.redactedDiagnostics) })
199
+ };
200
+ }
201
+ deleteReviewSessionTransactionMaterials(reviewSessionId) {
202
+ for (const [materialId, record] of this.records) {
203
+ if (record.reviewSessionId === reviewSessionId) {
204
+ this.records.delete(materialId);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ export function sameHandle(record, handle) {
210
+ return record.materialId === handle.materialId &&
211
+ record.reviewSessionId === handle.reviewSessionId &&
212
+ record.planId === handle.planId &&
213
+ record.account === handle.account &&
214
+ record.kind === handle.kind &&
215
+ record.source === handle.source &&
216
+ record.createdAt === handle.createdAt &&
217
+ record.expiresAt === handle.expiresAt;
218
+ }
219
+ function cloneBytes(bytes) {
220
+ return new Uint8Array(bytes);
221
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { suiAddressStringSchema, suiTransactionDigestSchema } from "../suiAddress.js";
3
+ import { suiChainReceiptEvidenceSchema } from "../action/suiChainReceiptEvidence.js";
4
+ export const TRANSACTION_REQUEST_STATUSES = [
5
+ "awaiting_signature", "submitting", "awaiting_chain_result", "stopped",
6
+ "request_failed", "outcome_unknown", "completed"
7
+ ];
8
+ export const transactionRequestStatusSchema = z.enum(TRANSACTION_REQUEST_STATUSES);
9
+ // The submission was admitted durably before external I/O. A failed follow-up
10
+ // write must not remove its initial observation window or replacement guard.
11
+ export const INITIAL_CHAIN_OBSERVATION_STATUSES = ["submitting", "awaiting_chain_result"];
12
+ export function isInitialChainObservation(status) {
13
+ return INITIAL_CHAIN_OBSERVATION_STATUSES.some((value) => value === status);
14
+ }
15
+ // This is the existing signing surface's local wait, independent of protocol
16
+ // request expiry, card input expiry and the chain observation window.
17
+ export const SIGNATURE_WAIT_MS = 90_000;
18
+ export const CHAIN_RECEIPT_LOOKUP_MAX_AGE_MS = 10 * 60 * 1000;
19
+ export const transactionExecutionSummarySchema = z.object({
20
+ reviewSessionId: z.string().min(1),
21
+ attemptId: z.string().min(1),
22
+ planId: z.string().min(1),
23
+ status: z.enum(["success", "failure"]),
24
+ txDigest: suiTransactionDigestSchema,
25
+ chainReceipt: suiChainReceiptEvidenceSchema,
26
+ failureReason: z.literal("chain_execution_failed").optional(),
27
+ recordedAt: z.string().datetime()
28
+ }).strict().superRefine((result, context) => {
29
+ if (result.txDigest !== result.chainReceipt.txDigest || (result.status === "success") !== result.chainReceipt.effectsStatus.success) {
30
+ context.addIssue({ code: "custom", message: "Execution must match the observed chain receipt." });
31
+ }
32
+ if ((result.status === "failure") !== (result.failureReason !== undefined)) {
33
+ context.addIssue({ code: "custom", message: "Only a chain failure has a chain execution failure reason." });
34
+ }
35
+ });
36
+ export const transactionRequestSchema = z.object({
37
+ attemptId: z.string().min(1),
38
+ reviewSessionId: z.string().min(1),
39
+ planId: z.string().min(1),
40
+ reviewRevision: z.number().int().nonnegative(),
41
+ account: suiAddressStringSchema,
42
+ transactionDigest: suiTransactionDigestSchema,
43
+ requestStatus: transactionRequestStatusSchema,
44
+ revision: z.number().int().nonnegative(),
45
+ createdAt: z.string().datetime(),
46
+ updatedAt: z.string().datetime(),
47
+ signatureVerifiedAt: z.string().datetime().optional(),
48
+ submittedAt: z.string().datetime().optional(),
49
+ reason: z.string().optional(),
50
+ execution: transactionExecutionSummarySchema.optional()
51
+ }).strict().superRefine((request, context) => {
52
+ const execution = request.execution;
53
+ if ((request.requestStatus === "completed") !== (execution !== undefined)) {
54
+ context.addIssue({ code: "custom", message: "Completed requests require an observed execution result." });
55
+ }
56
+ if (execution && (execution.attemptId !== request.attemptId || execution.reviewSessionId !== request.reviewSessionId ||
57
+ execution.planId !== request.planId || execution.txDigest !== request.transactionDigest || execution.chainReceipt.sender !== request.account)) {
58
+ context.addIssue({ code: "custom", message: "Execution identity differs from the admitted request." });
59
+ }
60
+ });
61
+ export function isRequestWaiting(status) {
62
+ return status === "awaiting_signature" || isInitialChainObservation(status);
63
+ }
64
+ export const requestTransitions = {
65
+ awaiting_signature: ["submitting", "stopped", "request_failed", "outcome_unknown"],
66
+ submitting: ["awaiting_chain_result", "outcome_unknown", "completed"],
67
+ awaiting_chain_result: ["outcome_unknown", "completed"],
68
+ stopped: [],
69
+ request_failed: [],
70
+ outcome_unknown: ["awaiting_chain_result", "completed"],
71
+ completed: []
72
+ };
@@ -0,0 +1,79 @@
1
+ import { EXECUTION_POLLING_INTERVAL_SECONDS, getExecutionPollingStatus, executionStatusCategory, isWaitStoppingExecutionStatus } from "./status.js";
2
+ import { SessionStoreError } from "./sessionStore.js";
3
+ import { workflowViewSchema } from "./workflowView.js";
4
+ import { WALLET_CONNECTION_POLL_SECONDS } from "./walletConnection.js";
5
+ export const DEFAULT_WAIT_TIMEOUT_MS = 45_000;
6
+ export const MAX_WAIT_TIMEOUT_MS = 55_000;
7
+ export const WAIT_OUTCOMES = ["status_reached", "timed_out", "unavailable"];
8
+ export class WaitRequestAbortedError extends Error {
9
+ reason;
10
+ constructor(reason = "host_abort") {
11
+ super("Wait request aborted");
12
+ this.reason = reason;
13
+ }
14
+ }
15
+ export async function waitForWalletConnection(cards, cardId, options = {}) {
16
+ const timeout = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
17
+ const started = Date.now(), sleep = options.sleep ?? sleepMs;
18
+ let snapshot;
19
+ while (true) {
20
+ assertNotAborted(options.signal);
21
+ snapshot = await cards.readSaved(cardId);
22
+ const view = workflowViewSchema.parse(snapshot.data);
23
+ if (snapshot.kind !== "connect")
24
+ throw new SessionStoreError("input_invalid", "A connection card ID is required.");
25
+ if (view.progress.status === "unavailable")
26
+ return { waitOutcome: "unavailable", snapshot };
27
+ if (!view.observe)
28
+ return { waitOutcome: "status_reached", snapshot };
29
+ const remaining = timeout - (Date.now() - started);
30
+ if (remaining <= 0)
31
+ return { waitOutcome: "timed_out", snapshot };
32
+ await sleep(Math.min(remaining, WALLET_CONNECTION_POLL_SECONDS * 1000), options.signal);
33
+ }
34
+ }
35
+ export async function waitForExecutionResult(readState, reviewSessionId, options = {}) {
36
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
37
+ const sleep = options.sleep ?? sleepMs, startedAt = Date.now();
38
+ let reason = "missing";
39
+ while (true) {
40
+ assertNotAborted(options.signal);
41
+ const state = await readState(reviewSessionId, options.now?.());
42
+ if (!state)
43
+ throw new SessionStoreError("session_not_found", `Review session not found: ${reviewSessionId}`, { reason });
44
+ const status = getExecutionPollingStatus(state.session, state.request);
45
+ if (executionStatusCategory(status) === "final")
46
+ return { ...state, status, waitOutcome: "status_reached" };
47
+ if (state.progress.status === "unavailable")
48
+ return { ...state, status, waitOutcome: "unavailable" };
49
+ if (isWaitStoppingExecutionStatus(status))
50
+ return { ...state, status, waitOutcome: "status_reached" };
51
+ const remaining = timeoutMs - (Date.now() - startedAt);
52
+ if (remaining <= 0)
53
+ return { ...state, status, waitOutcome: "timed_out" };
54
+ await sleep(Math.min(remaining, EXECUTION_POLLING_INTERVAL_SECONDS * 1000), options.signal);
55
+ reason = "session_removed_during_wait";
56
+ }
57
+ }
58
+ function sleepMs(ms, signal) {
59
+ return new Promise((resolve, reject) => {
60
+ if (signal?.aborted) {
61
+ reject(new WaitRequestAbortedError());
62
+ return;
63
+ }
64
+ const timer = setTimeout(() => {
65
+ signal?.removeEventListener("abort", onAbort);
66
+ resolve();
67
+ }, ms);
68
+ function onAbort() {
69
+ clearTimeout(timer);
70
+ reject(new WaitRequestAbortedError());
71
+ }
72
+ signal?.addEventListener("abort", onAbort, { once: true });
73
+ });
74
+ }
75
+ function assertNotAborted(signal) {
76
+ if (signal?.aborted) {
77
+ throw new WaitRequestAbortedError();
78
+ }
79
+ }
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ import { suiAddressStringSchema } from "../suiAddress.js";
3
+ export const SUI_MAINNET_WALLET_CHAIN = "sui:mainnet";
4
+ export const SUI_SIGN_TRANSACTION_METHOD = "sui_signTransaction";
5
+ export const WALLET_CONNECTION_POLL_SECONDS = 5;
6
+ export const walletRunIdSchema = z.string().uuid();
7
+ export const walletStartStageSchema = z.enum(["process_start", "sdk_start", "session_restore", "state_sync"]);
8
+ export const walletUnavailableReasonSchema = z.enum([
9
+ "initialization_failed", "restoration_failed", "wallet_state_unavailable"
10
+ ]);
11
+ export const walletAvailabilitySchema = z.discriminatedUnion("status", [
12
+ z.object({ status: z.literal("available"), walletRunId: walletRunIdSchema }).strict(),
13
+ z.object({ status: z.literal("initializing"), walletRunId: walletRunIdSchema, stage: walletStartStageSchema, message: z.string() }).strict(),
14
+ z.object({ status: z.literal("recovering"), walletRunId: walletRunIdSchema, message: z.string() }).strict(),
15
+ z.object({ status: z.literal("unavailable"), walletRunId: walletRunIdSchema.optional(), reason: walletUnavailableReasonSchema, message: z.string() }).strict()
16
+ ]);
17
+ export function walletUnavailable(reason, walletRunId) {
18
+ const message = reason === "restoration_failed"
19
+ ? "Wallet connections could not be restored."
20
+ : reason === "wallet_state_unavailable"
21
+ ? "The wallet connection status could not be checked. Saved transaction results remain available."
22
+ : "The wallet connection service could not start.";
23
+ return { status: "unavailable", reason, message, ...(walletRunId === undefined ? {} : { walletRunId }) };
24
+ }
25
+ export class WalletUnavailableError extends Error {
26
+ reason;
27
+ constructor(reason, message = walletUnavailable(reason).message) {
28
+ super(message);
29
+ this.reason = reason;
30
+ }
31
+ }
32
+ export const workflowProgressSchema = z.discriminatedUnion("status", [
33
+ z.object({ status: z.literal("idle") }).strict(),
34
+ z.object({ status: z.literal("waiting") }).strict(),
35
+ z.object({ status: z.literal("unavailable"), reason: z.literal("wallet_unavailable"), message: z.string() }).strict()
36
+ ]);
37
+ export function workflowProgress(waiting, requiresWallet, availability) {
38
+ if (!waiting)
39
+ return { status: "idle" };
40
+ if (requiresWallet && availability.status === "unavailable") {
41
+ return { status: "unavailable", reason: "wallet_unavailable", message: availability.message };
42
+ }
43
+ return { status: "waiting" };
44
+ }
45
+ export const walletConnectionStatusSchema = z.enum([
46
+ "awaiting_approval", "connected", "rejected", "failed", "expired", "stopped", "disconnected"
47
+ ]);
48
+ export const walletConnectionSchema = z.object({
49
+ connectionId: z.string().min(1),
50
+ status: walletConnectionStatusSchema,
51
+ revision: z.number().int().nonnegative(),
52
+ accounts: z.array(suiAddressStringSchema),
53
+ methods: z.array(z.string()),
54
+ chain: z.literal(SUI_MAINNET_WALLET_CHAIN),
55
+ createdAt: z.string().datetime(),
56
+ updatedAt: z.string().datetime(),
57
+ expiresAt: z.string().datetime(),
58
+ walletName: z.string().optional(),
59
+ reason: z.string().optional()
60
+ }).strict();
61
+ export const connectionConflictSchema = z.object({
62
+ reason: z.literal("multiple_connections"),
63
+ connectionIds: z.array(z.string().min(1)).min(2).refine((ids) => new Set(ids).size === ids.length)
64
+ }).strict();
65
+ export const CONNECTION_CONFLICT_MESSAGE = "More than one wallet connection is saved. Disconnect the connections you no longer need before continuing.";
66
+ export const assetReadAccountSchema = z.discriminatedUnion("status", [
67
+ z.object({ status: z.literal("available"), account: suiAddressStringSchema }).strict(),
68
+ z.object({ status: z.literal("address_required") }).strict()
69
+ ]);
70
+ // Management may name every recorded target. Account use requires exactly one
71
+ // live connection and no connection operation still awaiting its outcome.
72
+ export function walletConnectionSelection(connections, at, stored) {
73
+ const connected = connections.filter((item) => item.status === "connected" && Date.parse(item.expiresAt) > at);
74
+ const pending = connections.some((item) => item.pendingAction || item.status === "awaiting_approval");
75
+ const connection = connected.length === 1 && !pending ? connected[0] : undefined;
76
+ return {
77
+ ...(connected.length > 1 ? { conflict: { reason: "multiple_connections",
78
+ connectionIds: connected.map((item) => item.connectionId).sort() } } : {}),
79
+ ...(connection ? { connection } : {}),
80
+ pairingAllowed: connected.length === 0 && !pending,
81
+ assetReadAccount: connection && stored?.walletId === connection.connectionId && connection.accounts.includes(stored.address)
82
+ ? { status: "available", account: stored.address } : { status: "address_required" }
83
+ };
84
+ }
85
+ export class WalletUserRejectedError extends Error {
86
+ constructor() { super("The wallet request was rejected by the user."); }
87
+ }
88
+ // Shared recorded connection facts only. Each operation checks its own expiry,
89
+ // account, method and revision requirements at its decision boundary.
90
+ export function isOwnedConnectedWallet(record, ownerId, disconnectPending) {
91
+ return !!record && record.ownerId === ownerId && record.connection.status === "connected" && !disconnectPending;
92
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ import { suiAddressStringSchema } from "../suiAddress.js";
3
+ import { SUI_MAINNET_WALLET_CHAIN, walletRunIdSchema } from "./walletConnection.js";
4
+ export { walletRunIdSchema, walletStartStageSchema } from "./walletConnection.js";
5
+ export const walletSessionSchema = z.object({
6
+ topic: z.string().min(1), accounts: z.array(suiAddressStringSchema).min(1), methods: z.array(z.string()),
7
+ chain: z.literal(SUI_MAINNET_WALLET_CHAIN), expiresAt: z.string().datetime(), walletName: z.string().optional()
8
+ }).strict();
9
+ export const walletSessionObservationSchema = z.discriminatedUnion("status", [
10
+ z.object({ topic: z.string().min(1), version: z.number().int().positive(), status: z.literal("present"), session: walletSessionSchema }).strict(),
11
+ z.object({ topic: z.string().min(1), version: z.number().int().positive(), status: z.enum(["absent", "unusable"]) }).strict()
12
+ ]);
13
+ export const walletObservationSchema = z.object({
14
+ runId: walletRunIdSchema, sequence: z.number().int().positive(), observedAt: z.string().datetime()
15
+ }).strict();
16
+ export const walletSnapshotSchema = walletObservationSchema.extend({ sessions: z.array(walletSessionObservationSchema) }).strict();
17
+ const recoveryBase = z.object({ kind: z.literal("wallet_service_recovery"), priorRunId: walletRunIdSchema,
18
+ admittedAt: z.string().datetime(), updatedAt: z.string().datetime() });
19
+ export const walletRecoverySchema = z.union([
20
+ recoveryBase.extend({ phase: z.literal("stopping") }).strict(),
21
+ recoveryBase.extend({ phase: z.literal("starting"), nextRunId: walletRunIdSchema }).strict(),
22
+ recoveryBase.extend({ outcome: z.enum(["available", "failed", "superseded", "server_restarted"]),
23
+ nextRunId: walletRunIdSchema.optional(), message: z.string().optional() }).strict()
24
+ ]);
25
+ export class WalletRunInterruptedError extends Error {
26
+ runId;
27
+ constructor(runId) {
28
+ super("The wallet service run ended. This operation will not be repeated.");
29
+ this.runId = runId;
30
+ }
31
+ }
32
+ export class WalletReplacementFailedError extends Error {
33
+ runId;
34
+ constructor(runId) {
35
+ super("The wallet service restart could not be confirmed.");
36
+ this.runId = runId;
37
+ }
38
+ }