@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.
- package/LICENSE +21 -0
- package/LICENSES/@mysten-bcs-2.0.5-Apache-2.0.txt +205 -0
- package/LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt +201 -0
- package/LICENSES/@mysten-utils-0.3.3-Apache-2.0.txt +205 -0
- package/LICENSES/fancy-canvas-2.1.0-MIT.txt +7 -0
- package/LICENSES/lightweight-charts-5.2.0-NOTICE.txt +2 -0
- package/README.md +288 -0
- package/dist/adapters/adapterLifecycleValidators.js +5 -0
- package/dist/adapters/adapterPromptSurfaces.js +60 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +178 -0
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
- package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
- package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +254 -0
- package/dist/adapters/intentPlanFactories.js +51 -0
- package/dist/adapters/reviewAdapters.js +47 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/contractNameRegistry.js +151 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +193 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +71 -0
- package/dist/core/action/receiptPtbGraph.js +29 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +470 -0
- package/dist/core/action/schemas.js +491 -0
- package/dist/core/action/signableAdapterContract.js +998 -0
- package/dist/core/action/suiChainReceiptEvidence.js +106 -0
- package/dist/core/action/suiChainReceiptMapping.js +107 -0
- package/dist/core/action/suiChainReceiptReader.js +211 -0
- package/dist/core/action/suiChainReceiptVerifier.js +178 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +130 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +271 -0
- package/dist/core/action/transactionFunding.js +244 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +256 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +323 -0
- package/dist/core/action/types.js +33 -0
- package/dist/core/action/walletReviewContractAssembler.js +286 -0
- package/dist/core/activity/accountAssetTimeline.js +157 -0
- package/dist/core/activity/accountAssetTimelineUsdcReferences.js +120 -0
- package/dist/core/activity/activityStore.js +16 -0
- package/dist/core/activity/externalActivityCoverage.js +76 -0
- package/dist/core/activity/externalActivityTransactionStream.js +82 -0
- package/dist/core/activity/localDataService.js +296 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +439 -0
- package/dist/core/activity/schemaVersion.js +2 -0
- package/dist/core/activity/sqliteActivityStore.js +1068 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +389 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +367 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +186 -0
- package/dist/core/activity/transactionActivityAnalysis.js +295 -0
- package/dist/core/activity/transactionActivityClassifier.js +306 -0
- package/dist/core/activity/transactionActivityDetails.js +229 -0
- package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
- package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
- package/dist/core/activity/transactionActivityService.js +379 -0
- package/dist/core/activity/transactionActivityTypes.js +18 -0
- package/dist/core/eventlog/sink.js +35 -0
- package/dist/core/evidence/settlementFamilies.js +85 -0
- package/dist/core/evidence/userAnswerUse.js +1 -0
- package/dist/core/numeric/rawU64.js +63 -0
- package/dist/core/preferences/preferencesStore.js +26 -0
- package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
- package/dist/core/proposal/externalProposalReview.js +347 -0
- package/dist/core/proposal/schemas.js +208 -0
- package/dist/core/proposal/types.js +35 -0
- package/dist/core/read/amounts.js +14 -0
- package/dist/core/read/coinMetadata.js +60 -0
- package/dist/core/read/deepbookOfficialIndexerSource.js +373 -0
- package/dist/core/read/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +321 -0
- package/dist/core/read/deepbookRegistry.js +157 -0
- package/dist/core/read/deepbookSourceOwners.js +159 -0
- package/dist/core/read/deepbookUsdcChartService.js +342 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readCardInputs.js +19 -0
- package/dist/core/read/readCardResult.js +40 -0
- package/dist/core/read/readResponseGuidance.js +402 -0
- package/dist/core/read/readService.js +1639 -0
- package/dist/core/read/readServiceTypes.js +88 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +45 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/cardSession.js +31 -0
- package/dist/core/session/cardSessionStore.js +204 -0
- package/dist/core/session/keyedRecordStore.js +18 -0
- package/dist/core/session/liveReviewSessionContract.js +3 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +75 -0
- package/dist/core/session/reviewValidity.js +49 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionRecordStore.js +40 -0
- package/dist/core/session/sessionStore.js +655 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +45 -0
- package/dist/core/session/sqliteCardStore.js +88 -0
- package/dist/core/session/sqliteSessionStore.js +213 -0
- package/dist/core/session/sqliteTransactionMaterialStore.js +64 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +821 -0
- package/dist/core/session/status.js +60 -0
- package/dist/core/session/transactionMaterialStore.js +221 -0
- package/dist/core/session/transactionRequest.js +72 -0
- package/dist/core/session/wait.js +79 -0
- package/dist/core/session/walletConnection.js +92 -0
- package/dist/core/session/walletRuntime.js +38 -0
- package/dist/core/session/walletWorkflow.js +608 -0
- package/dist/core/session/workflowState.js +80 -0
- package/dist/core/session/workflowView.js +150 -0
- package/dist/core/suiAddress.js +18 -0
- package/dist/core/suiEndpoint.js +72 -0
- package/dist/mcp/activeAccountResponse.js +24 -0
- package/dist/mcp/prompts.js +146 -0
- package/dist/mcp/registerTool.js +19 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/responseGuidance.js +326 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +11 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/serverInfo.js +131 -0
- package/dist/mcp/toolErrors.js +118 -0
- package/dist/mcp/toolNames.js +58 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +126 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +686 -0
- package/dist/mcp/tools/read/index.js +14 -0
- package/dist/mcp/tools/read/readToolHelpers.js +97 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +179 -0
- package/dist/mcp/tools/read/serverStatusTools.js +100 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +371 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +858 -0
- package/dist/mcp/tools/read/walletReadTools.js +734 -0
- package/dist/mcp/tools/session/executionResultTools.js +44 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +36 -0
- package/dist/mcp/tools/session/statusTools.js +69 -0
- package/dist/mcp/tools/session/walletConnectionTools.js +69 -0
- package/dist/mcp/tools/settings/index.js +66 -0
- package/dist/mcp-app/account.html +3133 -0
- package/dist/mcp-app/account.notices.txt +973 -0
- package/dist/mcp-app/chart.html +3354 -0
- package/dist/mcp-app/chart.notices.txt +1194 -0
- package/dist/mcp-app/connect.html +3179 -0
- package/dist/mcp-app/connect.notices.txt +1012 -0
- package/dist/mcp-app/receipt.html +8430 -0
- package/dist/mcp-app/receipt.notices.txt +3549 -0
- package/dist/mcp-app/review.html +8430 -0
- package/dist/mcp-app/review.notices.txt +3549 -0
- package/dist/mcp-ui/contracts.js +31 -0
- package/dist/mcp-ui/readCards.js +28 -0
- package/dist/mcp-ui/tools.js +134 -0
- package/dist/review-app/brand-dark.svg +1 -0
- package/dist/review-app/brand-light.svg +1 -0
- package/dist/review-app/favicon.svg +1 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/ui.css +809 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +45 -0
- package/dist/review-server/http.js +47 -0
- package/dist/review-server/middleware/hostOrigin.js +48 -0
- package/dist/review-server/middleware/reviewToken.js +7 -0
- package/dist/review-server/reviewServerPolicy.js +1 -0
- package/dist/review-server/server.js +72 -0
- package/dist/review-server/settingsApi.js +158 -0
- package/dist/runtime/application.js +203 -0
- package/dist/runtime/config.js +97 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +66 -0
- package/dist/runtime/reviewDependencies.js +58 -0
- package/dist/runtime/reviewServerAcquire.js +93 -0
- package/dist/runtime/runtimeTermination.js +60 -0
- package/dist/runtime/shared/authenticatedFetch.js +127 -0
- package/dist/runtime/shared/control.js +93 -0
- package/dist/runtime/shared/dataAccess.js +66 -0
- package/dist/runtime/shared/mcpHttp.js +84 -0
- package/dist/runtime/shared/ownerLease.js +50 -0
- package/dist/runtime/shared/server.js +98 -0
- package/dist/runtime/shared/stdio.js +113 -0
- package/dist/runtime/smokeMainnetRead.js +391 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +306 -0
- package/dist/runtime/smokeReview.js +40 -0
- package/dist/runtime/start.js +90 -0
- package/dist/runtime/suiEndpoint.js +97 -0
- package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
- package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
- package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
- package/dist/runtime/walletConnectConfig.js +3 -0
- package/dist/runtime/walletConnectStorage.js +99 -0
- package/dist/runtime/walletConnectTransport.js +128 -0
- package/dist/runtime/walletSdkChild.js +3 -0
- package/dist/runtime/walletSdkChildRuntime.js +160 -0
- package/dist/runtime/walletSdkIpc.js +26 -0
- package/dist/runtime/walletSdkProcess.js +358 -0
- package/docs/AGENT_BEHAVIOR.md +524 -0
- package/docs/AGENT_DEVELOPMENT_POLICY.md +815 -0
- package/docs/FRONTEND_POLICY.md +534 -0
- package/docs/LOCAL_DB_ARCHITECTURE.md +381 -0
- package/docs/MCP_SETUP.md +742 -0
- package/docs/MCP_TOOLS.md +1318 -0
- package/docs/SDK_API.md +206 -0
- package/docs/SIGNABLE_ADAPTER_CONTRACT.md +396 -0
- package/docs/TRANSACTION_ACTIVITY_LOG.md +194 -0
- package/docs/UTILITY_INDEX.md +275 -0
- package/docs/WALLET_CONNECTION.md +238 -0
- package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +166 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +161 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +259 -0
- package/package.json +79 -0
- package/protocols/deepbook-margin.md +15 -0
- 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
|
+
}
|