@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,106 @@
1
+ import { z } from "zod";
2
+ import { makeSignedRawIntegerStringSchema, parseSignedRawInteger } from "../numeric/rawU64.js";
3
+ import { normalizeCoinType } from "../read/coinMetadata.js";
4
+ import { suiAddressStringSchema, suiTransactionDigestSchema } from "../suiAddress.js";
5
+ export const SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE = [
6
+ "transaction",
7
+ "effects",
8
+ "balanceChanges",
9
+ "objectTypes"
10
+ ];
11
+ const isoUtcStringSchema = z.string().refine((value) => {
12
+ const parsed = new Date(value);
13
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
14
+ }, "Expected ISO 8601 UTC timestamp");
15
+ const suiChainReceiptIncludeFieldSchema = z.enum(SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE);
16
+ const suiChainReceiptIncludeSchema = z.array(suiChainReceiptIncludeFieldSchema)
17
+ .length(SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE.length)
18
+ .superRefine((include, ctx) => {
19
+ for (const field of SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE) {
20
+ if (!include.includes(field)) {
21
+ ctx.addIssue({
22
+ code: "custom",
23
+ path: [],
24
+ message: `chain receipt include is missing '${field}'`
25
+ });
26
+ }
27
+ }
28
+ if (new Set(include).size !== include.length) {
29
+ ctx.addIssue({
30
+ code: "custom",
31
+ path: [],
32
+ message: "chain receipt include fields must not be duplicated"
33
+ });
34
+ }
35
+ });
36
+ const coinTypeSchema = z.string().min(1).max(512).refine((value) => {
37
+ try {
38
+ normalizeCoinType(value);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }, "Expected a Sui struct tag coin type");
45
+ const suiChainReceiptPackageCallSchema = z.object({
46
+ commandIndex: z.number().int().nonnegative(),
47
+ packageId: suiAddressStringSchema,
48
+ module: z.string().min(1).max(128),
49
+ function: z.string().min(1).max(128),
50
+ target: z.string().min(1).max(512)
51
+ }).strict().superRefine((call, ctx) => {
52
+ const expectedTarget = `${call.packageId}::${call.module}::${call.function}`;
53
+ if (call.target !== expectedTarget) {
54
+ ctx.addIssue({
55
+ code: "custom",
56
+ path: ["target"],
57
+ message: "package call target must match packageId::module::function"
58
+ });
59
+ }
60
+ });
61
+ const suiChainReceiptAccountBalanceChangeSchema = z.object({
62
+ index: z.number().int().nonnegative(),
63
+ coinType: coinTypeSchema,
64
+ amountRaw: makeSignedRawIntegerStringSchema("accountBalanceChanges[].amountRaw"),
65
+ direction: z.enum(["increase", "decrease", "zero"])
66
+ }).strict().superRefine((change, ctx) => {
67
+ const amount = parseSignedRawInteger(change.amountRaw, "accountBalanceChanges[].amountRaw");
68
+ const direction = amount > 0n ? "increase" :
69
+ amount < 0n ? "decrease" :
70
+ "zero";
71
+ if (change.direction !== direction) {
72
+ ctx.addIssue({
73
+ code: "custom",
74
+ path: ["direction"],
75
+ message: "balance change direction must match amountRaw sign"
76
+ });
77
+ }
78
+ });
79
+ const suiChainReceiptEffectsStatusSchema = z.discriminatedUnion("success", [
80
+ z.object({
81
+ success: z.literal(true),
82
+ errorKind: z.never().optional(),
83
+ errorMessage: z.never().optional()
84
+ }).strict(),
85
+ z.object({
86
+ success: z.literal(false),
87
+ errorKind: z.string().min(1).max(200).optional(),
88
+ errorMessage: z.string().min(1).max(2000).optional()
89
+ }).strict()
90
+ ]);
91
+ export const suiChainReceiptEvidenceSchema = z.object({
92
+ kind: z.literal("sui_chain_receipt_v1"),
93
+ source: z.object({
94
+ method: z.literal("client.core.getTransaction"),
95
+ network: z.literal("sui:mainnet"),
96
+ chainIdentifier: z.string().min(1),
97
+ fetchedAt: isoUtcStringSchema,
98
+ include: suiChainReceiptIncludeSchema
99
+ }).strict(),
100
+ txDigest: suiTransactionDigestSchema,
101
+ sender: suiAddressStringSchema,
102
+ effectsStatus: suiChainReceiptEffectsStatusSchema,
103
+ packageCalls: z.array(suiChainReceiptPackageCallSchema),
104
+ accountBalanceChanges: z.array(suiChainReceiptAccountBalanceChangeSchema),
105
+ objectTypes: z.record(suiAddressStringSchema, z.string().min(1).max(512))
106
+ }).strict();
@@ -0,0 +1,107 @@
1
+ import { parseSuiAddress } from "../suiAddress.js";
2
+ // Shared, pure mappers from the SDK getTransaction result to chain-receipt facts.
3
+ // Both the session-bound verifier (suiChainReceiptVerifier.ts) and the public
4
+ // receipt reader (suiChainReceiptReader.ts) use these so the effects-status,
5
+ // Move-call, and object-type shapes are mapped in one place. Balance changes are
6
+ // not here because the verifier filters them to the reviewed account while the
7
+ // public reader keeps them all, so each owns its own balance mapper.
8
+ export function effectsStatusFromSdk(status) {
9
+ if (status.success) {
10
+ return { success: true };
11
+ }
12
+ const error = status.error;
13
+ const errorKind = typeof error === "object" && error !== null && typeof error.$kind === "string"
14
+ ? error.$kind.slice(0, 200)
15
+ : undefined;
16
+ const errorMessage = typeof error === "object" && error !== null && typeof error.message === "string" && error.message.length > 0
17
+ ? error.message.slice(0, 2000)
18
+ : undefined;
19
+ return {
20
+ success: false,
21
+ ...(errorKind === undefined ? {} : { errorKind }),
22
+ ...(errorMessage === undefined ? {} : { errorMessage })
23
+ };
24
+ }
25
+ export function packageCallsFromTransaction(transaction) {
26
+ return transaction.commands.flatMap((command, commandIndex) => {
27
+ const moveCall = moveCallFromCommand(command);
28
+ if (!moveCall) {
29
+ return [];
30
+ }
31
+ const packageId = parseSuiAddress(moveCall.package);
32
+ if (!packageId) {
33
+ throw new Error("Sui mainnet receipt contains a Move call with an invalid package id.");
34
+ }
35
+ return [
36
+ {
37
+ commandIndex,
38
+ packageId,
39
+ module: moveCall.module,
40
+ function: moveCall.function,
41
+ target: `${packageId}::${moveCall.module}::${moveCall.function}`
42
+ }
43
+ ];
44
+ });
45
+ }
46
+ export function objectTypesFromSdk(objectTypes) {
47
+ return Object.fromEntries(Object.entries(objectTypes).map(([objectId, objectType]) => {
48
+ const normalizedObjectId = parseSuiAddress(objectId);
49
+ if (!normalizedObjectId) {
50
+ throw new Error("Sui mainnet receipt contains an invalid object id in objectTypes.");
51
+ }
52
+ return [normalizedObjectId, objectType];
53
+ }));
54
+ }
55
+ // Fail-closed address parsing for receipt facts. Both the verifier and the
56
+ // public reader turn a raw chain address into a receipt address ONLY through
57
+ // this helper, so neither path can silently drop or null out a balance-change
58
+ // address or sender when the SDK payload is not the expected shape. A malformed
59
+ // address throws; callers map the throw to a fail-closed receipt outcome rather
60
+ // than returning partial facts.
61
+ export function requireReceiptAddress(rawAddress, errorMessage) {
62
+ const address = rawAddress === null || rawAddress === undefined ? undefined : parseSuiAddress(rawAddress);
63
+ if (!address) {
64
+ throw new Error(errorMessage);
65
+ }
66
+ return address;
67
+ }
68
+ // Shared error classification for the getTransaction read, used by both the
69
+ // verifier and the public reader so a "transaction not found" is recognized one
70
+ // way everywhere.
71
+ export function isReceiptNotFoundError(error) {
72
+ const code = typeof error === "object" && error !== null
73
+ ? (error.code ?? error.status)
74
+ : undefined;
75
+ if (code === "NOT_FOUND" || code === "NotFound" || code === "NOT_FOUND_ERROR" || code === 5) {
76
+ return true;
77
+ }
78
+ // Only genuine "this digest does not exist / is not indexed" signals map to
79
+ // not_found. Availability/outage phrasing (e.g. "service not available") is a
80
+ // provider failure and must stay an `unavailable` (502), not a 404.
81
+ return /\b(not found|not indexed|could not find|unable to find|no transaction)\b/i.test(receiptErrorMessage(error, ""));
82
+ }
83
+ export function receiptErrorMessage(error, fallback) {
84
+ if (error instanceof Error) {
85
+ return error.message || fallback;
86
+ }
87
+ if (typeof error === "string") {
88
+ return error || fallback;
89
+ }
90
+ if (typeof error === "object" && error !== null) {
91
+ const message = error.message;
92
+ if (typeof message === "string" && message.length > 0) {
93
+ return message;
94
+ }
95
+ }
96
+ return fallback;
97
+ }
98
+ function moveCallFromCommand(command) {
99
+ const maybeCommand = command;
100
+ if (!maybeCommand.MoveCall) {
101
+ return undefined;
102
+ }
103
+ if (maybeCommand.$kind !== undefined && maybeCommand.$kind !== "MoveCall") {
104
+ return undefined;
105
+ }
106
+ return maybeCommand.MoveCall;
107
+ }
@@ -0,0 +1,211 @@
1
+ import { readCoinReservation } from "./transactionFunding.js";
2
+ import { normalizeCoinType } from "../read/coinMetadata.js";
3
+ import { suiTransactionDigestSchema } from "../suiAddress.js";
4
+ import { receiptPtbMermaid } from "./receiptPtbGraph.js";
5
+ import { effectsStatusFromSdk, isReceiptNotFoundError, objectTypesFromSdk, packageCallsFromTransaction, receiptErrorMessage, requireReceiptAddress } from "./suiChainReceiptMapping.js";
6
+ import { SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE } from "./suiChainReceiptEvidence.js";
7
+ const PUBLIC_RECEIPT_INCLUDE = {
8
+ ...Object.fromEntries(SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE.map((field) => [field, true])),
9
+ events: true
10
+ };
11
+ export async function readPublicChainReceipt(options, input) {
12
+ const digest = suiTransactionDigestSchema.safeParse(input.digest);
13
+ if (!digest.success) {
14
+ return { status: "invalid_digest" };
15
+ }
16
+ if (options.network !== "mainnet") {
17
+ return { status: "unavailable", message: "Receipt reads require a verified Sui mainnet endpoint." };
18
+ }
19
+ let chainIdentifier;
20
+ try {
21
+ chainIdentifier = (await options.client.core.getChainIdentifier()).chainIdentifier;
22
+ }
23
+ catch (error) {
24
+ return { status: "unavailable", message: receiptErrorMessage(error, "Sui mainnet chain identifier lookup failed.") };
25
+ }
26
+ if (chainIdentifier !== options.expectedChainIdentifier) {
27
+ return { status: "unavailable", message: "Receipt reads require a verified Sui mainnet endpoint." };
28
+ }
29
+ let result;
30
+ try {
31
+ result = await options.client.core.getTransaction({ digest: digest.data, include: PUBLIC_RECEIPT_INCLUDE });
32
+ }
33
+ catch (error) {
34
+ if (isReceiptNotFoundError(error)) {
35
+ return { status: "not_found" };
36
+ }
37
+ return { status: "unavailable", message: receiptErrorMessage(error, "Sui mainnet transaction lookup failed.") };
38
+ }
39
+ const transaction = result.$kind === "Transaction" ? result.Transaction : result.FailedTransaction;
40
+ const transactionData = transaction.transaction;
41
+ const effects = transaction.effects;
42
+ if (!transactionData || !effects || !transaction.balanceChanges || !transaction.objectTypes) {
43
+ return { status: "unavailable", message: "Sui mainnet receipt is missing required fields." };
44
+ }
45
+ // Build receipt facts fail-closed: any malformed field (sender, Move calls,
46
+ // balance-change address, object id) throws inside this block and maps to an
47
+ // `unavailable` outcome, so the public receipt is never returned with silently
48
+ // dropped or nulled facts and the mappers can never crash the request. The PTB
49
+ // graph is best-effort and falls back to undefined (placeholder shown), so it is
50
+ // produced outside this block and never fails the receipt.
51
+ let receipt;
52
+ try {
53
+ receipt = {
54
+ txDigest: transaction.digest,
55
+ // A present-but-unparseable sender is a malformed payload and fails closed.
56
+ // An absent sender is legitimate for some system transactions, so it maps
57
+ // to undefined. The account-bound verifier instead requires a sender
58
+ // because it must match the reviewed account.
59
+ sender: transactionData.sender === null || transactionData.sender === undefined
60
+ ? undefined
61
+ : requireReceiptAddress(transactionData.sender, "Sui mainnet receipt has an unparseable sender address."),
62
+ effectsStatus: effectsStatusFromSdk(effects.status),
63
+ packageCalls: packageCallsFromTransaction(transactionData),
64
+ balanceChanges: balanceChangesFromSdk(transaction.balanceChanges),
65
+ objectTypes: objectTypesFromSdk(transaction.objectTypes),
66
+ gas: gasFromSdk(effects.gasUsed, transactionData.gasData),
67
+ inputs: inputsFromSdk(transactionData.inputs ?? []),
68
+ events: eventsFromSdk(transaction.events ?? []),
69
+ ptbGraph: ptbGraphFromTransaction(transactionData),
70
+ chainIdentifier,
71
+ fetchedAt: input.now.toISOString()
72
+ };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ status: "unavailable",
77
+ message: receiptErrorMessage(error, "Sui mainnet receipt could not be read as expected facts.")
78
+ };
79
+ }
80
+ // Best-effort decimals enrichment: resolved outside the fail-closed block so a
81
+ // coin-metadata miss or outage leaves the balance change on its raw amount and
82
+ // never turns a readable receipt into an error.
83
+ if (options.resolveCoinUnit && receipt.balanceChanges.length > 0) {
84
+ receipt = { ...receipt, balanceChanges: await withCoinDecimals(receipt.balanceChanges, options.resolveCoinUnit) };
85
+ }
86
+ return { status: "found", receipt };
87
+ }
88
+ async function withCoinDecimals(changes, resolveCoinUnit) {
89
+ const units = new Map();
90
+ await Promise.all([...new Set(changes.map((change) => change.coinType))].map(async (coinType) => {
91
+ try {
92
+ units.set(coinType, await resolveCoinUnit(coinType));
93
+ }
94
+ catch {
95
+ // Best-effort only: leave the coin without decimals.
96
+ }
97
+ }));
98
+ return changes.map((change) => {
99
+ const unit = units.get(change.coinType);
100
+ return unit && unit.status === "available"
101
+ ? { ...change, decimals: unit.decimals, symbol: unit.symbol }
102
+ : change;
103
+ });
104
+ }
105
+ function balanceChangesFromSdk(balanceChanges) {
106
+ return balanceChanges.map((change, index) => {
107
+ const address = requireReceiptAddress(change.address, "Sui mainnet receipt contains a balance change with an invalid address.");
108
+ const amount = BigInt(change.amount);
109
+ return {
110
+ index,
111
+ address,
112
+ coinType: normalizeCoinType(change.coinType),
113
+ amountRaw: amount.toString(),
114
+ direction: amount > 0n ? "increase" : amount < 0n ? "decrease" : "zero"
115
+ };
116
+ });
117
+ }
118
+ function gasFromSdk(gasUsed, gasData) {
119
+ const computation = BigInt(gasUsed.computationCost);
120
+ const storage = BigInt(gasUsed.storageCost);
121
+ const rebate = BigInt(gasUsed.storageRebate);
122
+ return {
123
+ totalMist: (computation + storage - rebate).toString(),
124
+ computationMist: computation.toString(),
125
+ storageMist: storage.toString(),
126
+ storageRebateMist: rebate.toString(),
127
+ nonRefundableStorageMist: BigInt(gasUsed.nonRefundableStorageFee).toString(),
128
+ budgetMist: gasData?.budget === null || gasData?.budget === undefined ? undefined : BigInt(gasData.budget).toString(),
129
+ priceMist: gasData?.price === null || gasData?.price === undefined ? undefined : BigInt(gasData.price).toString(),
130
+ paymentObjectId: firstPaymentObjectId(gasData)
131
+ };
132
+ }
133
+ function firstPaymentObjectId(gasData) {
134
+ const payment = gasData?.payment;
135
+ if (!Array.isArray(payment) || payment.length === 0) {
136
+ return undefined;
137
+ }
138
+ for (const item of payment) {
139
+ const ref = asRecord(item);
140
+ if (typeof ref?.objectId !== "string" || typeof ref.digest !== "string")
141
+ continue;
142
+ try {
143
+ if (!readCoinReservation(ref.digest))
144
+ return ref.objectId;
145
+ }
146
+ catch { /* Invalid refs do not establish gas object facts. */ }
147
+ }
148
+ return undefined;
149
+ }
150
+ // PTB inputs, simplified to a kind and (for object inputs) an object id. The exact
151
+ // SDK enum shape is read defensively so a shape change degrades to `unknown`
152
+ // rather than throwing.
153
+ function inputsFromSdk(inputs) {
154
+ return inputs.map((raw, index) => {
155
+ const input = asRecord(raw);
156
+ if (input) {
157
+ if ("Object" in input) {
158
+ const object = asRecord(input.Object);
159
+ const variant = asRecord(object?.ImmOrOwnedObject) ?? asRecord(object?.SharedObject) ?? asRecord(object?.Receiving) ?? object;
160
+ const objectId = typeof variant?.objectId === "string" ? variant.objectId : undefined;
161
+ const kind = object && "SharedObject" in object ? "shared_object" : object && "Receiving" in object ? "receiving" : "object";
162
+ return { index, kind, objectId };
163
+ }
164
+ if ("UnresolvedObject" in input) {
165
+ const object = asRecord(input.UnresolvedObject);
166
+ return { index, kind: "object", objectId: typeof object?.objectId === "string" ? object.objectId : undefined };
167
+ }
168
+ if ("Pure" in input || "UnresolvedPure" in input) {
169
+ const pure = asRecord(input.Pure) ?? asRecord(input.UnresolvedPure);
170
+ const bytes = pureBytesToHex(pure?.bytes);
171
+ return bytes === undefined
172
+ ? { index, kind: "pure", objectId: undefined }
173
+ : { index, kind: "pure", objectId: undefined, bytes };
174
+ }
175
+ if ("FundsWithdrawal" in input) {
176
+ return { index, kind: "withdrawal", objectId: undefined };
177
+ }
178
+ }
179
+ return { index, kind: "unknown", objectId: undefined };
180
+ });
181
+ }
182
+ function eventsFromSdk(events) {
183
+ return events.map((event, index) => ({
184
+ index,
185
+ packageId: event.packageId,
186
+ module: event.module,
187
+ eventType: event.eventType,
188
+ sender: event.sender
189
+ }));
190
+ }
191
+ function ptbGraphFromTransaction(transactionData) {
192
+ const mermaid = receiptPtbMermaid({ inputs: transactionData.inputs, commands: transactionData.commands });
193
+ return mermaid === undefined ? undefined : { mermaid };
194
+ }
195
+ function asRecord(value) {
196
+ return typeof value === "object" && value !== null ? value : undefined;
197
+ }
198
+ // A pure input's bytes arrive base64-encoded; surface them as 0x-hex (the
199
+ // convention used elsewhere in the UI). Returns undefined for an absent or
200
+ // non-string value so the input simply carries no bytes.
201
+ function pureBytesToHex(value) {
202
+ if (typeof value !== "string" || value.length === 0) {
203
+ return undefined;
204
+ }
205
+ try {
206
+ return `0x${Buffer.from(value, "base64").toString("hex")}`;
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ }
@@ -0,0 +1,178 @@
1
+ import { assertNoForbiddenMcpFields } from "./forbiddenFields.js";
2
+ import { SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE, suiChainReceiptEvidenceSchema } from "./suiChainReceiptEvidence.js";
3
+ import { normalizeCoinType } from "../read/coinMetadata.js";
4
+ import { parseSuiAddress, suiTransactionDigestSchema } from "../suiAddress.js";
5
+ import { effectsStatusFromSdk, isReceiptNotFoundError, objectTypesFromSdk, packageCallsFromTransaction, receiptErrorMessage, requireReceiptAddress } from "./suiChainReceiptMapping.js";
6
+ export const SUI_CHAIN_RECEIPT_GET_TRANSACTION_INCLUDE = includeObjectFromFields(SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE);
7
+ // Bounded per-read wait for the known digest to become visible on this node.
8
+ // This existing 12-second source-read window is independent of the backend's
9
+ // 10-minute initial observation deadline. A missing receipt does not establish
10
+ // whether a prior submit (including one with a lost reply) executed.
11
+ export const SUI_CHAIN_RECEIPT_WAIT_TIMEOUT_MS = 12_000;
12
+ // waitForTransaction aborts with a DOMException named "TimeoutError" when its wait window elapses.
13
+ // Treat that exactly like not_found — the transaction is simply not readable here yet — so the
14
+ // session stays pending for a retry rather than being marked a verification failure.
15
+ function isWaitTimeoutError(error) {
16
+ return typeof error === "object" && error !== null && error.name === "TimeoutError";
17
+ }
18
+ export async function verifySuiChainReceipt(options, input) {
19
+ const parsedInput = parseVerifierInput(input);
20
+ if (!parsedInput.ok) {
21
+ return verificationFailed(parsedInput.message);
22
+ }
23
+ if (options.network !== "mainnet") {
24
+ return verificationFailed("Chain receipt verification requires a verified Sui mainnet endpoint.");
25
+ }
26
+ let chainIdentifier;
27
+ try {
28
+ const chainIdentifierResult = await options.client.core.getChainIdentifier();
29
+ chainIdentifier = chainIdentifierResult.chainIdentifier;
30
+ }
31
+ catch (error) {
32
+ return verificationFailed(receiptErrorMessage(error, "Sui mainnet chain identifier lookup failed."));
33
+ }
34
+ if (chainIdentifier !== options.expectedChainIdentifier) {
35
+ return verificationFailed("Chain receipt verification requires a verified Sui mainnet endpoint.");
36
+ }
37
+ let result;
38
+ try {
39
+ // The signed transaction was just submitted: it is on chain but may not yet be queryable on
40
+ // this server's fullnode (checkpoint-propagation lag). Wait for it instead of returning a
41
+ // premature not_found, so a single review-page poll resolves straight to the final receipt.
42
+ result = await options.client.core.waitForTransaction({
43
+ digest: parsedInput.txDigest,
44
+ include: SUI_CHAIN_RECEIPT_GET_TRANSACTION_INCLUDE,
45
+ timeout: SUI_CHAIN_RECEIPT_WAIT_TIMEOUT_MS
46
+ });
47
+ }
48
+ catch (error) {
49
+ if (isReceiptNotFoundError(error) || isWaitTimeoutError(error)) {
50
+ return {
51
+ status: "not_found",
52
+ failureReason: "chain_receipt_unavailable",
53
+ message: "Sui mainnet did not return a transaction for the signed digest within the wait window."
54
+ };
55
+ }
56
+ return verificationFailed(receiptErrorMessage(error, "Sui mainnet transaction lookup failed."));
57
+ }
58
+ try {
59
+ const transaction = result.$kind === "Transaction" ? result.Transaction : result.FailedTransaction;
60
+ const receipt = createSuiChainReceiptEvidenceFromTransaction({
61
+ transaction,
62
+ txDigest: parsedInput.txDigest,
63
+ reviewedTransactionDigest: parsedInput.reviewedTransactionDigest,
64
+ account: parsedInput.account,
65
+ chainIdentifier,
66
+ fetchedAt: parsedInput.now
67
+ });
68
+ if (!receipt.effectsStatus.success) {
69
+ return {
70
+ status: "verified_failure",
71
+ failureReason: "chain_execution_failed",
72
+ message: "Sui mainnet verified the transaction receipt and reported failed execution effects.",
73
+ receipt
74
+ };
75
+ }
76
+ return { status: "verified_success", receipt };
77
+ }
78
+ catch (error) {
79
+ return verificationFailed(receiptErrorMessage(error, "Sui mainnet transaction receipt could not be verified."));
80
+ }
81
+ }
82
+ function includeObjectFromFields(fields) {
83
+ return Object.fromEntries(fields.map((field) => [field, true]));
84
+ }
85
+ function parseVerifierInput(input) {
86
+ if (!(input.now instanceof Date) || !Number.isFinite(input.now.getTime())) {
87
+ return { ok: false, message: "Chain receipt fetchedAt time must be a valid Date." };
88
+ }
89
+ const txDigest = suiTransactionDigestSchema.safeParse(input.txDigest);
90
+ if (!txDigest.success) {
91
+ return { ok: false, message: "Signed transaction digest is not a valid Sui transaction digest." };
92
+ }
93
+ const reviewedTransactionDigest = suiTransactionDigestSchema.safeParse(input.reviewedTransactionDigest);
94
+ if (!reviewedTransactionDigest.success) {
95
+ return { ok: false, message: "Reviewed transaction digest is not a valid Sui transaction digest." };
96
+ }
97
+ const account = parseSuiAddress(input.account);
98
+ if (!account) {
99
+ return { ok: false, message: "Reviewed account is not a valid Sui address." };
100
+ }
101
+ return {
102
+ ok: true,
103
+ txDigest: txDigest.data,
104
+ reviewedTransactionDigest: reviewedTransactionDigest.data,
105
+ account,
106
+ now: input.now
107
+ };
108
+ }
109
+ function createSuiChainReceiptEvidenceFromTransaction(input) {
110
+ if (input.txDigest !== input.reviewedTransactionDigest) {
111
+ throw new Error("Signed transaction digest does not match the reviewed transaction digest.");
112
+ }
113
+ if (input.transaction.digest !== input.txDigest) {
114
+ throw new Error("Sui mainnet returned a transaction digest that does not match the signed digest.");
115
+ }
116
+ const transactionData = input.transaction.transaction;
117
+ if (!transactionData) {
118
+ throw new Error("Sui mainnet receipt is missing parsed transaction data.");
119
+ }
120
+ const effects = input.transaction.effects;
121
+ if (!effects) {
122
+ throw new Error("Sui mainnet receipt is missing transaction effects.");
123
+ }
124
+ if (effects.transactionDigest !== input.txDigest) {
125
+ throw new Error("Sui mainnet receipt effects digest does not match the signed digest.");
126
+ }
127
+ const sender = requireReceiptAddress(transactionData.sender, "Sui mainnet receipt is missing a valid sender.");
128
+ if (sender !== input.account) {
129
+ throw new Error("Sui mainnet receipt sender does not match the reviewed account.");
130
+ }
131
+ if (!input.transaction.balanceChanges) {
132
+ throw new Error("Sui mainnet receipt is missing balance changes.");
133
+ }
134
+ if (!input.transaction.objectTypes) {
135
+ throw new Error("Sui mainnet receipt is missing object type map.");
136
+ }
137
+ const receipt = {
138
+ kind: "sui_chain_receipt_v1",
139
+ source: {
140
+ method: "client.core.getTransaction",
141
+ network: "sui:mainnet",
142
+ chainIdentifier: input.chainIdentifier,
143
+ fetchedAt: input.fetchedAt.toISOString(),
144
+ include: [...SUI_CHAIN_RECEIPT_REQUIRED_INCLUDE]
145
+ },
146
+ txDigest: input.txDigest,
147
+ sender,
148
+ effectsStatus: effectsStatusFromSdk(effects.status),
149
+ packageCalls: packageCallsFromTransaction(transactionData),
150
+ accountBalanceChanges: accountBalanceChangesFromSdk(input.transaction.balanceChanges, input.account),
151
+ objectTypes: objectTypesFromSdk(input.transaction.objectTypes)
152
+ };
153
+ const parsed = suiChainReceiptEvidenceSchema.parse(receipt);
154
+ assertNoForbiddenMcpFields(parsed);
155
+ return parsed;
156
+ }
157
+ function accountBalanceChangesFromSdk(balanceChanges, account) {
158
+ return balanceChanges.flatMap((change, index) => {
159
+ const address = requireReceiptAddress(change.address, "Sui mainnet receipt contains a balance change with an invalid address.");
160
+ if (address !== account) {
161
+ return [];
162
+ }
163
+ const amount = BigInt(change.amount);
164
+ return [{
165
+ index,
166
+ coinType: normalizeCoinType(change.coinType),
167
+ amountRaw: amount.toString(),
168
+ direction: amount > 0n ? "increase" : amount < 0n ? "decrease" : "zero"
169
+ }];
170
+ });
171
+ }
172
+ function verificationFailed(message) {
173
+ return {
174
+ status: "verification_failed",
175
+ failureReason: "receipt_verification_failed",
176
+ message
177
+ };
178
+ }