@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,130 @@
1
+ import { transactionFundingSummary } from "./transactionFunding.js";
2
+ import { assertPreSimulationHumanReadableReviewBoundaryClaims, createHumanReadableReviewEvidence, verifyHumanReadableReviewEvidence } from "./humanReadableReviewEvidence.js";
3
+ import { parseSuiAddress } from "../suiAddress.js";
4
+ export function createSwapHumanReadableReviewEvidence(input) {
5
+ return verifySwapHumanReadableReviewEvidence({
6
+ transactionMaterial: input.transactionMaterial,
7
+ transactionMaterialDigest: input.transactionMaterialDigest,
8
+ swapQuotePolicy: input.swapQuotePolicy,
9
+ transactionObjectOwnership: input.transactionObjectOwnership,
10
+ evidence: createHumanReadableReviewEvidence({
11
+ transactionMaterial: input.transactionMaterial,
12
+ transactionMaterialDigest: input.transactionMaterialDigest,
13
+ adapterId: input.adapterId,
14
+ protocol: input.protocol,
15
+ actionKind: input.actionKind,
16
+ review: input.review,
17
+ derivedAt: input.derivedAt
18
+ }),
19
+ now: input.derivedAt
20
+ });
21
+ }
22
+ export function verifySwapHumanReadableReviewEvidence(input) {
23
+ const evidence = verifyHumanReadableReviewEvidence({
24
+ transactionMaterial: input.transactionMaterial,
25
+ transactionMaterialDigest: input.transactionMaterialDigest,
26
+ evidence: input.evidence,
27
+ now: input.now
28
+ });
29
+ if (evidence.review.kind !== "swap_human_readable_review") {
30
+ throw new Error("swap human-readable review evidence requires a swap_human_readable_review projection");
31
+ }
32
+ if (input.swapQuotePolicy.adapterId !== evidence.adapterId ||
33
+ input.swapQuotePolicy.protocol !== evidence.protocol ||
34
+ input.swapQuotePolicy.actionKind !== evidence.actionKind) {
35
+ throw new Error("swap human-readable review evidence must match swap quote policy adapter identity");
36
+ }
37
+ for (const artifact of [input.swapQuotePolicy, input.transactionObjectOwnership]) {
38
+ if (artifact.materialId !== input.transactionMaterial.materialId ||
39
+ artifact.reviewSessionId !== input.transactionMaterial.reviewSessionId ||
40
+ artifact.planId !== input.transactionMaterial.planId ||
41
+ artifact.account !== input.transactionMaterial.account ||
42
+ artifact.expiresAt !== input.transactionMaterial.expiresAt) {
43
+ throw new Error("swap human-readable review evidence requires quote policy and object ownership artifacts for the same material");
44
+ }
45
+ }
46
+ if (input.transactionObjectOwnership.transactionDigest !== input.transactionMaterialDigest.transactionDigest) {
47
+ throw new Error("swap human-readable review evidence requires object ownership bound to the same transaction digest");
48
+ }
49
+ const fundingFacts = evidence.review.evidenceUsed.filter((fact) => fact.id === "transaction_funding");
50
+ if (fundingFacts.length !== 1 || fundingFacts[0]?.source !== "transaction_material" ||
51
+ fundingFacts[0]?.summary !== transactionFundingSummary(input.transactionObjectOwnership.funding)) {
52
+ throw new Error("swap human-readable review must describe the verified transaction funding");
53
+ }
54
+ assertSwapHumanReadableReviewProjectsQuotePolicy(evidence, input.swapQuotePolicy);
55
+ assertSwapHumanReadableReviewParties(evidence);
56
+ assertSwapHumanReadableReviewEvidenceSources(evidence);
57
+ assertSwapHumanReadableReviewBoundaryClaims(evidence);
58
+ assertPreSimulationHumanReadableReviewBoundaryClaims(evidence);
59
+ return evidence;
60
+ }
61
+ function assertSwapHumanReadableReviewProjectsQuotePolicy(evidence, swapQuotePolicy) {
62
+ assertSingleAmountProjection("outgoing", evidence.review.assetFlow.outgoing, "input", swapQuotePolicy.sourceAmount);
63
+ assertSingleAmountProjection("expected incoming", evidence.review.assetFlow.expectedIncoming, "expected_output", swapQuotePolicy.expectedOutput);
64
+ assertSingleAmountProjection("minimum incoming", evidence.review.assetFlow.minimumIncoming, "minimum_output", swapQuotePolicy.minimumOutput);
65
+ assertSingleAmountProjection("fee", evidence.review.assetFlow.fees, "fee", swapQuotePolicy.protocolFee);
66
+ if (evidence.review.targets.length !== 1) {
67
+ throw new Error("swap human-readable review targets must contain exactly one swap output asset target");
68
+ }
69
+ const target = evidence.review.targets[0];
70
+ if (target.kind !== "swap_output_asset" ||
71
+ target.symbol !== swapQuotePolicy.expectedOutput.asset.symbol ||
72
+ target.coinType !== swapQuotePolicy.expectedOutput.asset.coinType ||
73
+ target.protocol !== swapQuotePolicy.protocol ||
74
+ target.poolKey !== swapQuotePolicy.quoteSource.poolKey ||
75
+ target.direction !== swapQuotePolicy.quoteSource.direction) {
76
+ throw new Error("swap human-readable review target must match swap quote policy output asset and quote source");
77
+ }
78
+ }
79
+ function assertSwapHumanReadableReviewParties(evidence) {
80
+ const recipientsByRole = new Map();
81
+ for (const recipient of evidence.review.recipients) {
82
+ if (recipientsByRole.has(recipient.role)) {
83
+ throw new Error(`swap human-readable review recipients contains duplicate role '${recipient.role}'`);
84
+ }
85
+ const normalizedAddress = parseSuiAddress(recipient.address);
86
+ if (!normalizedAddress) {
87
+ throw new Error("swap human-readable review recipients must contain valid Sui addresses");
88
+ }
89
+ recipientsByRole.set(recipient.role, normalizedAddress);
90
+ }
91
+ if (recipientsByRole.size !== 2) {
92
+ throw new Error("swap human-readable review recipients must contain exactly the connected account and output recipient");
93
+ }
94
+ if (recipientsByRole.get("connected_account") !== evidence.account ||
95
+ recipientsByRole.get("output_recipient") !== evidence.account) {
96
+ throw new Error("swap human-readable review recipients must match the reviewed account");
97
+ }
98
+ }
99
+ function assertSwapHumanReadableReviewEvidenceSources(evidence) {
100
+ for (const source of ["quote", "wallet"]) {
101
+ requireFactSource(evidence.review.evidenceUsed, source);
102
+ }
103
+ }
104
+ function assertSwapHumanReadableReviewBoundaryClaims(evidence) {
105
+ requireGapId("unsupportedClaims", evidence.review.unsupportedClaims, "no_route_recommendation");
106
+ }
107
+ function assertSingleAmountProjection(label, amounts, role, expected) {
108
+ if (amounts.length !== 1) {
109
+ throw new Error(`swap human-readable review ${label} must contain exactly one amount`);
110
+ }
111
+ const amount = amounts[0];
112
+ if (amount.role !== role ||
113
+ amount.rawAmount !== expected.raw ||
114
+ amount.rawAmountSource !== "quote_policy_evidence" ||
115
+ amount.symbol !== expected.asset.symbol ||
116
+ amount.coinType !== expected.asset.coinType ||
117
+ amount.decimals !== expected.asset.decimals) {
118
+ throw new Error(`swap human-readable review ${label} amount must match swap quote policy evidence`);
119
+ }
120
+ }
121
+ function requireFactSource(facts, source) {
122
+ if (!facts.some((fact) => fact.source === source)) {
123
+ throw new Error(`swap human-readable review evidenceUsed must include source '${source}'`);
124
+ }
125
+ }
126
+ function requireGapId(label, gaps, id) {
127
+ if (!gaps.some((gap) => gap.id === id)) {
128
+ throw new Error(`swap human-readable review ${label} must include '${id}'`);
129
+ }
130
+ }
@@ -0,0 +1,271 @@
1
+ import { z } from "zod";
2
+ import { adapterEvidenceClaimSchema, adapterRawQuantitySchema, adapterSourceOfTruthSchema } from "./signableAdapterContract.js";
3
+ import { parseRawU64 } from "../numeric/rawU64.js";
4
+ import { normalizedSuiAddressSchema } from "../suiAddress.js";
5
+ import { DEEPBOOK_SCALAR_UNIT_SOURCE, SUI_METADATA_UNIT_SOURCE, normalizeCoinType } from "../read/coinMetadata.js";
6
+ export const SWAP_QUOTE_POLICY_EVIDENCE_VERSION = "swap-quote-policy-v1";
7
+ const BPS_DENOMINATOR = 10000n;
8
+ const isoUtcStringSchema = z.string().refine((value) => {
9
+ const parsed = new Date(value);
10
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
11
+ }, "Expected ISO 8601 UTC timestamp");
12
+ const coinTypeSchema = z.string().min(1).max(512).refine((value) => {
13
+ try {
14
+ normalizeCoinType(value);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }, "Expected a Sui struct tag coin type").transform((value) => normalizeCoinType(value));
21
+ const quotePolicyAssetSchema = z.object({
22
+ symbol: z.string().min(1).max(64),
23
+ coinType: coinTypeSchema,
24
+ decimals: z.number().int().min(0).max(255),
25
+ unitSource: z.enum([DEEPBOOK_SCALAR_UNIT_SOURCE, SUI_METADATA_UNIT_SOURCE])
26
+ }).strict();
27
+ const quotePolicyRawAmountSchema = z.object({
28
+ raw: z.string().min(1),
29
+ asset: quotePolicyAssetSchema
30
+ }).strict();
31
+ export const swapQuotePolicyEvidenceSchema = z.object({
32
+ evidenceVersion: z.literal(SWAP_QUOTE_POLICY_EVIDENCE_VERSION),
33
+ materialId: z.string().min(1),
34
+ reviewSessionId: z.string().min(1),
35
+ planId: z.string().min(1),
36
+ account: normalizedSuiAddressSchema,
37
+ kind: z.literal("swap_quote_policy"),
38
+ adapterId: z.string().min(1).max(120),
39
+ protocol: z.string().min(1).max(120),
40
+ actionKind: z.string().min(1).max(120),
41
+ network: z.literal("sui:mainnet"),
42
+ quoteEvidenceId: z.string().min(1).max(120),
43
+ quoteSource: z.object({
44
+ provider: z.string().min(1).max(120),
45
+ poolKey: z.string().min(1).max(120),
46
+ direction: z.enum(["base_to_quote", "quote_to_base"]),
47
+ fetchedAt: isoUtcStringSchema,
48
+ sourceMoveFunction: z.string().min(1).max(160)
49
+ }).strict(),
50
+ policySource: z.literal("adapter_policy_from_quote_evidence"),
51
+ maxSlippageBps: z.number().int().min(1).max(10_000),
52
+ staleAfterMs: z.number().int().min(1),
53
+ sourceAmount: quotePolicyRawAmountSchema,
54
+ expectedOutput: quotePolicyRawAmountSchema,
55
+ minimumOutput: quotePolicyRawAmountSchema,
56
+ protocolFee: quotePolicyRawAmountSchema,
57
+ derivedAt: isoUtcStringSchema,
58
+ expiresAt: isoUtcStringSchema
59
+ }).strict();
60
+ export function createSwapQuotePolicyEvidence(input) {
61
+ return verifySwapQuotePolicyEvidence({
62
+ transactionMaterial: input.materialHandle,
63
+ evidence: {
64
+ evidenceVersion: SWAP_QUOTE_POLICY_EVIDENCE_VERSION,
65
+ materialId: input.materialHandle.materialId,
66
+ reviewSessionId: input.materialHandle.reviewSessionId,
67
+ planId: input.materialHandle.planId,
68
+ account: input.materialHandle.account,
69
+ kind: "swap_quote_policy",
70
+ adapterId: input.adapterId,
71
+ protocol: input.protocol,
72
+ actionKind: input.actionKind,
73
+ network: "sui:mainnet",
74
+ quoteEvidenceId: input.quoteEvidenceId,
75
+ quoteSource: input.quoteSource,
76
+ policySource: "adapter_policy_from_quote_evidence",
77
+ maxSlippageBps: input.maxSlippageBps,
78
+ staleAfterMs: input.staleAfterMs,
79
+ sourceAmount: input.sourceAmount,
80
+ expectedOutput: input.expectedOutput,
81
+ minimumOutput: input.minimumOutput,
82
+ protocolFee: input.protocolFee,
83
+ derivedAt: input.derivedAt.toISOString(),
84
+ expiresAt: input.materialHandle.expiresAt
85
+ },
86
+ now: input.derivedAt
87
+ });
88
+ }
89
+ export function parseSwapQuotePolicyEvidence(value) {
90
+ return swapQuotePolicyEvidenceSchema.parse(value);
91
+ }
92
+ export function verifySwapQuotePolicyEvidence(input) {
93
+ const evidence = parseSwapQuotePolicyEvidence(input.evidence);
94
+ const now = input.now ?? new Date();
95
+ const nowMs = now.getTime();
96
+ if (!Number.isFinite(nowMs)) {
97
+ throw new Error("now must be a valid Date");
98
+ }
99
+ if (evidence.materialId !== input.transactionMaterial.materialId ||
100
+ evidence.reviewSessionId !== input.transactionMaterial.reviewSessionId ||
101
+ evidence.planId !== input.transactionMaterial.planId ||
102
+ evidence.account !== input.transactionMaterial.account ||
103
+ evidence.expiresAt !== input.transactionMaterial.expiresAt) {
104
+ throw new Error("swap quote policy evidence must match material identity");
105
+ }
106
+ const fetchedAtMs = Date.parse(evidence.quoteSource.fetchedAt);
107
+ const derivedAtMs = Date.parse(evidence.derivedAt);
108
+ const expiresAtMs = Date.parse(evidence.expiresAt);
109
+ if (fetchedAtMs > derivedAtMs) {
110
+ throw new Error("swap quote policy evidence fetchedAt must not be after derivedAt");
111
+ }
112
+ if (Date.parse(evidence.quoteSource.fetchedAt) + evidence.staleAfterMs !== expiresAtMs) {
113
+ throw new Error("swap quote policy evidence expiresAt must equal fetchedAt plus staleAfterMs");
114
+ }
115
+ if (derivedAtMs > nowMs) {
116
+ throw new Error("swap quote policy evidence derivedAt must not be in the future");
117
+ }
118
+ if (expiresAtMs <= derivedAtMs || expiresAtMs <= nowMs) {
119
+ throw new Error("swap quote policy evidence must not be expired");
120
+ }
121
+ const expectedOutput = parseRawU64(evidence.expectedOutput.raw, "expectedOutput.raw", { positive: true });
122
+ const minimumOutput = parseRawU64(evidence.minimumOutput.raw, "minimumOutput.raw", { positive: true });
123
+ parseRawU64(evidence.sourceAmount.raw, "sourceAmount.raw", { positive: true });
124
+ parseRawU64(evidence.protocolFee.raw, "protocolFee.raw");
125
+ const computedMinimum = (expectedOutput * (BPS_DENOMINATOR - BigInt(evidence.maxSlippageBps))) / BPS_DENOMINATOR;
126
+ if (minimumOutput !== computedMinimum) {
127
+ throw new Error("swap quote policy minimumOutput.raw must match expectedOutput.raw and maxSlippageBps");
128
+ }
129
+ if (!sameAsset(evidence.expectedOutput.asset, evidence.minimumOutput.asset)) {
130
+ throw new Error("swap quote policy minimum output asset must match expected output asset");
131
+ }
132
+ const contractMapping = mapSwapQuotePolicyEvidenceToContractDraft(evidence);
133
+ if (contractMapping.status === "unsupported") {
134
+ throw new Error(`swap quote policy evidence is not contract-mappable: ${contractMapping.reason}`);
135
+ }
136
+ return evidence;
137
+ }
138
+ export function mapSwapQuotePolicyEvidenceToContractDraft(evidenceInput) {
139
+ const evidence = parseSwapQuotePolicyEvidence(evidenceInput);
140
+ const outputUnitSource = mapUnitSource(evidence.expectedOutput.asset.unitSource);
141
+ if (!outputUnitSource) {
142
+ return { status: "unsupported", reason: "unsupported output unit source" };
143
+ }
144
+ const quoteSourceId = "swap_quote_policy";
145
+ const outputUnitSourceId = "swap_quote_output_unit";
146
+ const outputUnitClaimId = "swap_quote_output_unit_claim";
147
+ const expectedAmountClaimId = "swap_quote_expected_output_claim";
148
+ const minimumAmountClaimId = "swap_quote_minimum_output_claim";
149
+ const quoteMinOutClaimId = "swap_quote_min_out_claim";
150
+ const slippagePolicyClaimId = "swap_quote_slippage_policy_claim";
151
+ const outputAsset = {
152
+ symbol: evidence.expectedOutput.asset.symbol,
153
+ coinType: evidence.expectedOutput.asset.coinType
154
+ };
155
+ const sourceReferences = [
156
+ adapterSourceOfTruthSchema.parse({
157
+ id: quoteSourceId,
158
+ kind: "quote_evidence",
159
+ network: "sui:mainnet",
160
+ source: `${evidence.protocol} quote and slippage rules (adapter: ${evidence.adapterId})`,
161
+ verifiedAt: evidence.derivedAt,
162
+ fields: ["quoteEvidenceId", "rawAmount", "minOutRaw", "asset", "amountRole", "maxSlippageBps"]
163
+ }),
164
+ adapterSourceOfTruthSchema.parse({
165
+ id: outputUnitSourceId,
166
+ kind: outputUnitSource.sourceReferencesKind,
167
+ network: "sui:mainnet",
168
+ source: outputUnitSource.sourceDescription,
169
+ verifiedAt: evidence.derivedAt,
170
+ fields: ["coinType", "decimals"]
171
+ })
172
+ ];
173
+ const evidenceClaims = [
174
+ adapterEvidenceClaimSchema.parse({
175
+ id: expectedAmountClaimId,
176
+ factKind: "raw_quantity_amount",
177
+ sourceEvidenceId: quoteSourceId,
178
+ role: "expected_output",
179
+ asset: outputAsset,
180
+ rawAmount: evidence.expectedOutput.raw
181
+ }),
182
+ adapterEvidenceClaimSchema.parse({
183
+ id: minimumAmountClaimId,
184
+ factKind: "raw_quantity_amount",
185
+ sourceEvidenceId: quoteSourceId,
186
+ role: "minimum_output",
187
+ asset: outputAsset,
188
+ rawAmount: evidence.minimumOutput.raw
189
+ }),
190
+ adapterEvidenceClaimSchema.parse({
191
+ id: outputUnitClaimId,
192
+ factKind: "unit_metadata",
193
+ sourceEvidenceId: outputUnitSourceId,
194
+ source: outputUnitSource.unitClaimSource,
195
+ coinType: outputAsset.coinType,
196
+ decimals: evidence.expectedOutput.asset.decimals
197
+ }),
198
+ adapterEvidenceClaimSchema.parse({
199
+ id: quoteMinOutClaimId,
200
+ factKind: "quote_min_out",
201
+ sourceEvidenceId: quoteSourceId,
202
+ quoteEvidenceId: evidence.quoteEvidenceId,
203
+ minOutRaw: evidence.minimumOutput.raw
204
+ }),
205
+ adapterEvidenceClaimSchema.parse({
206
+ id: slippagePolicyClaimId,
207
+ factKind: "slippage_policy",
208
+ sourceEvidenceId: quoteSourceId,
209
+ policySource: "adapter_policy_from_quote_evidence",
210
+ maxSlippageBps: evidence.maxSlippageBps,
211
+ minOutRaw: evidence.minimumOutput.raw
212
+ })
213
+ ];
214
+ const rawQuantities = [
215
+ adapterRawQuantitySchema.parse({
216
+ id: "swap_quote_expected_output",
217
+ role: "expected_output",
218
+ asset: outputAsset,
219
+ rawAmount: evidence.expectedOutput.raw,
220
+ unit: {
221
+ decimals: evidence.expectedOutput.asset.decimals,
222
+ source: outputUnitSource.unitClaimSource,
223
+ sourceField: "expectedOutput.asset.decimals",
224
+ unitClaimId: outputUnitClaimId
225
+ },
226
+ amountClaimId: expectedAmountClaimId
227
+ }),
228
+ adapterRawQuantitySchema.parse({
229
+ id: "swap_quote_minimum_output",
230
+ role: "minimum_output",
231
+ asset: outputAsset,
232
+ rawAmount: evidence.minimumOutput.raw,
233
+ unit: {
234
+ decimals: evidence.expectedOutput.asset.decimals,
235
+ source: outputUnitSource.unitClaimSource,
236
+ sourceField: "minimumOutput.asset.decimals",
237
+ unitClaimId: outputUnitClaimId
238
+ },
239
+ amountClaimId: minimumAmountClaimId
240
+ })
241
+ ];
242
+ return {
243
+ status: "mapped",
244
+ sourceReferences,
245
+ evidenceClaims,
246
+ rawQuantities
247
+ };
248
+ }
249
+ function sameAsset(left, right) {
250
+ return (left.symbol === right.symbol &&
251
+ left.coinType === right.coinType &&
252
+ left.decimals === right.decimals &&
253
+ left.unitSource === right.unitSource);
254
+ }
255
+ function mapUnitSource(unitSource) {
256
+ if (unitSource === DEEPBOOK_SCALAR_UNIT_SOURCE) {
257
+ return {
258
+ sourceReferencesKind: "pinned_sdk_registry",
259
+ unitClaimSource: "pinned_sdk_metadata",
260
+ sourceDescription: "Token decimals from the pinned DeepBook mainnet metadata, used to interpret raw quote amounts"
261
+ };
262
+ }
263
+ if (unitSource === "sui_core_getCoinMetadata") {
264
+ return {
265
+ sourceReferencesKind: "verified_mainnet_onchain_metadata",
266
+ unitClaimSource: "verified_mainnet_onchain_metadata",
267
+ sourceDescription: "Token decimals read from Sui mainnet, used to interpret raw quote amounts"
268
+ };
269
+ }
270
+ return undefined;
271
+ }
@@ -0,0 +1,244 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { z } from "zod";
3
+ import { bcs, TypeTagSerializer } from "@mysten/sui/bcs";
4
+ import { deriveDynamicFieldID, fromBase58, fromHex, normalizeSuiAddress, toHex, SUI_TYPE_ARG } from "@mysten/sui/utils";
5
+ import { normalizeCoinType } from "../read/coinMetadata.js";
6
+ import { normalizedSuiAddressSchema } from "../suiAddress.js";
7
+ import { makeCanonicalRawU64StringSchema } from "../numeric/rawU64.js";
8
+ const raw = makeCanonicalRawU64StringSchema("funding amount");
9
+ const address = normalizedSuiAddressSchema;
10
+ const coinType = z.string().refine((value) => {
11
+ try {
12
+ return normalizeCoinType(value) === value;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ });
18
+ const reference = z.object({ objectId: address, version: raw, digest: z.string() }).strict();
19
+ const expiration = z.discriminatedUnion("kind", [
20
+ z.object({ kind: z.literal("none") }).strict(),
21
+ z.object({ kind: z.literal("epoch"), epoch: raw }).strict(),
22
+ z.object({ kind: z.literal("valid_during"), minEpoch: raw, maxEpoch: raw,
23
+ chain: z.string().min(1), nonce: z.number().int().min(0).max(0xffffffff) }).strict()
24
+ ]);
25
+ export const transactionFundingDescriptionSchema = z.object({
26
+ sender: address, gasOwner: address, chainIdentifier: z.string().min(1),
27
+ gasBudgetRaw: raw, gasPriceRaw: raw,
28
+ gasMode: z.enum(["coin_objects", "address_balance", "coin_reservation"]),
29
+ gasPayments: z.array(reference),
30
+ gasObjectIds: z.array(address),
31
+ reservations: z.array(z.object({ objectId: address, amountRaw: raw, epoch: raw }).strict()),
32
+ withdrawals: z.array(z.object({ inputIndex: z.number().int().min(0), owner: address,
33
+ coinType, maxAmountRaw: raw }).strict()),
34
+ expiration
35
+ }).strict();
36
+ const addressBalance = z.object({ owner: address, coinType,
37
+ // A holding can exceed the u64 limit of an individual transaction input.
38
+ balanceRaw: z.string().regex(/^(0|[1-9][0-9]*)$/), requiredRaw: z.string().regex(/^(0|[1-9][0-9]*)$/)
39
+ }).strict();
40
+ export const transactionFundingEvidenceSchema = transactionFundingDescriptionSchema.extend({
41
+ observedEpoch: raw.optional(), addressBalances: z.array(addressBalance)
42
+ }).superRefine((value, ctx) => {
43
+ try {
44
+ assertFundingEvidence(value);
45
+ }
46
+ catch (error) {
47
+ ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : "Invalid funding evidence" });
48
+ }
49
+ });
50
+ export class TransactionFundingError extends Error {
51
+ kind;
52
+ constructor(kind, message) {
53
+ super(message);
54
+ this.kind = kind;
55
+ }
56
+ }
57
+ // Reservation-format portions are adapted from Mysten Labs (Apache-2.0).
58
+ // Copyright (c) Mysten Labs, Inc. See LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt.
59
+ // Sui mainnet-v1.72.5 coin_reservation.rs and @mysten/sui 2.17.0
60
+ // utils/coin-reservation.ts define this layout. The SDK helper is not exported.
61
+ // Decode only: coin selection and reservation creation remain SDK/node work.
62
+ export function readCoinReservation(digest) {
63
+ const bytes = fromBase58(digest);
64
+ if (bytes.length !== 32)
65
+ throw new Error("Invalid payment reference digest length");
66
+ if (!bytes.slice(12).every((byte) => byte === 0xac))
67
+ return undefined;
68
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
69
+ return { amountRaw: view.getBigUint64(0, true).toString(), epoch: view.getUint32(8, true).toString() };
70
+ }
71
+ function reservationId(owner, chain) {
72
+ const key = TypeTagSerializer.parseFromStr("0x2::accumulator::Key<0x2::balance::Balance<0x2::sui::SUI>>");
73
+ const id = deriveDynamicFieldID(normalizeSuiAddress("0xacc"), key, bcs.Address.serialize(owner).toBytes());
74
+ const result = fromHex(id.slice(2)), mask = fromBase58(chain);
75
+ if (mask.length !== 32)
76
+ throw new Error("Invalid funding chain identifier");
77
+ for (let i = 0; i < result.length; i++)
78
+ result[i] = result[i] ^ mask[i];
79
+ return normalizeSuiAddress(toHex(result));
80
+ }
81
+ function record(value) {
82
+ if (!value || typeof value !== "object")
83
+ throw new Error("Missing funding transaction field");
84
+ return value;
85
+ }
86
+ export function describeTransactionFunding(data, account, chainIdentifier) {
87
+ if (!data.sender || !data.gasData.owner || !Array.isArray(data.gasData.payment))
88
+ throw new Error("Missing sender, gas owner or gas payments");
89
+ const sender = normalizeSuiAddress(data.sender), gasOwner = normalizeSuiAddress(data.gasData.owner);
90
+ if (sender !== account || gasOwner !== account)
91
+ throw new Error("Transaction funding must belong to the reviewed account");
92
+ const gasPayments = data.gasData.payment.map((item) => reference.parse(item));
93
+ const gasObjectIds = [], reservations = [];
94
+ for (const payment of gasPayments) {
95
+ const reserved = readCoinReservation(payment.digest);
96
+ if (reserved) {
97
+ if (payment.objectId !== reservationId(account, chainIdentifier) || reserved.amountRaw === "0")
98
+ throw new Error("Invalid account-bound SUI reservation");
99
+ reservations.push({ objectId: payment.objectId, ...reserved });
100
+ }
101
+ else
102
+ gasObjectIds.push(payment.objectId);
103
+ }
104
+ if (new Set(gasPayments.map((item) => item.objectId)).size !== gasPayments.length)
105
+ throw new Error("Duplicate gas payment reference");
106
+ const withdrawals = [];
107
+ for (const [inputIndex, input] of data.inputs.entries()) {
108
+ const item = record(input);
109
+ if (item.$kind === "FundsWithdrawal" || "FundsWithdrawal" in item) {
110
+ const withdrawal = record(item.FundsWithdrawal);
111
+ if (withdrawal.withdrawFrom?.Sender !== true || withdrawal.reservation?.MaxAmountU64 === undefined ||
112
+ typeof withdrawal.typeArg?.Balance !== "string")
113
+ throw new Error("Unsupported address-balance withdrawal");
114
+ const maxAmountRaw = raw.parse(withdrawal.reservation.MaxAmountU64);
115
+ if (maxAmountRaw === "0")
116
+ throw new Error("Address-balance withdrawal must be positive");
117
+ withdrawals.push({ inputIndex, owner: account, coinType: normalizeCoinType(withdrawal.typeArg.Balance), maxAmountRaw });
118
+ }
119
+ }
120
+ let expiry = { kind: "none" };
121
+ if (data.expiration) {
122
+ const value = record(data.expiration);
123
+ if (value.ValidDuring) {
124
+ const during = value.ValidDuring;
125
+ if (during.minTimestamp != null || during.maxTimestamp != null)
126
+ throw new Error("Timestamp expiration is unsupported");
127
+ expiry = expiration.parse({ kind: "valid_during", minEpoch: during.minEpoch, maxEpoch: during.maxEpoch, chain: during.chain, nonce: during.nonce });
128
+ if (expiry.kind !== "valid_during" || expiry.chain !== chainIdentifier || BigInt(expiry.maxEpoch) < BigInt(expiry.minEpoch) ||
129
+ BigInt(expiry.maxEpoch) > BigInt(expiry.minEpoch) + 1n)
130
+ throw new Error("Invalid mainnet epoch window");
131
+ }
132
+ else if (value.Epoch !== undefined) {
133
+ const epoch = value.Epoch;
134
+ // The pinned SDK's BCS Epoch decoder returns a number, unlike ValidDuring.
135
+ if (typeof epoch === "number" && (!Number.isSafeInteger(epoch) || epoch < 0)) {
136
+ throw new Error("Transaction expiration epoch must be a non-negative safe integer");
137
+ }
138
+ expiry = expiration.parse({ kind: "epoch", epoch: typeof epoch === "number" ? String(epoch) : epoch });
139
+ }
140
+ else if (!value.None)
141
+ throw new Error("Unsupported transaction expiration");
142
+ }
143
+ const gasMode = gasPayments.length === 0 ? "address_balance" : reservations.length ? "coin_reservation" : "coin_objects";
144
+ if (gasMode === "address_balance" && expiry.kind !== "valid_during")
145
+ throw new Error("Address-balance gas requires ValidDuring expiration");
146
+ const description = transactionFundingDescriptionSchema.parse({ sender, gasOwner, chainIdentifier,
147
+ gasBudgetRaw: data.gasData.budget, gasPriceRaw: data.gasData.price, gasMode, gasPayments, gasObjectIds, reservations, withdrawals, expiration: expiry });
148
+ if (BigInt(description.gasBudgetRaw) === 0n || BigInt(description.gasPriceRaw) === 0n)
149
+ throw new Error("Gasless transactions are unsupported");
150
+ return description;
151
+ }
152
+ export function addressFundingRequirements(funding) {
153
+ const amounts = new Map();
154
+ for (const withdrawal of funding.withdrawals)
155
+ amounts.set(withdrawal.coinType, (amounts.get(withdrawal.coinType) ?? 0n) + BigInt(withdrawal.maxAmountRaw));
156
+ const gasReservation = funding.gasMode === "address_balance" ? BigInt(funding.gasBudgetRaw)
157
+ : funding.reservations.reduce((sum, item) => sum + BigInt(item.amountRaw), 0n);
158
+ if (gasReservation > 0n)
159
+ amounts.set(SUI_TYPE_ARG, (amounts.get(SUI_TYPE_ARG) ?? 0n) + gasReservation);
160
+ return amounts;
161
+ }
162
+ function assertFundingEvidence(value) {
163
+ if (value.sender !== value.gasOwner)
164
+ throw new Error("Funding account mismatch");
165
+ const objects = [], reservations = [];
166
+ for (const payment of value.gasPayments) {
167
+ const reserved = readCoinReservation(payment.digest);
168
+ if (reserved) {
169
+ if (payment.objectId !== reservationId(value.sender, value.chainIdentifier) || reserved.amountRaw === "0")
170
+ throw new Error("Invalid funding reservation");
171
+ reservations.push({ objectId: payment.objectId, ...reserved });
172
+ }
173
+ else
174
+ objects.push(payment.objectId);
175
+ }
176
+ const mode = value.gasPayments.length === 0 ? "address_balance" : reservations.length ? "coin_reservation" : "coin_objects";
177
+ if (value.gasMode !== mode || !isDeepStrictEqual(objects, value.gasObjectIds) || !isDeepStrictEqual(reservations, value.reservations) ||
178
+ new Set(value.gasPayments.map((item) => item.objectId)).size !== value.gasPayments.length ||
179
+ new Set(value.withdrawals.map((item) => item.inputIndex)).size !== value.withdrawals.length ||
180
+ value.withdrawals.some((item) => item.owner !== value.sender || item.maxAmountRaw === "0") ||
181
+ value.gasBudgetRaw === "0" || value.gasPriceRaw === "0")
182
+ throw new Error("Inconsistent funding description");
183
+ const expected = addressFundingRequirements(value);
184
+ if (value.addressBalances.length !== expected.size || new Set(value.addressBalances.map((item) => item.coinType)).size !== expected.size)
185
+ throw new Error("Incomplete address-balance evidence");
186
+ for (const balance of value.addressBalances) {
187
+ if (balance.owner !== value.sender || expected.get(balance.coinType)?.toString() !== balance.requiredRaw || BigInt(balance.balanceRaw) < BigInt(balance.requiredRaw))
188
+ throw new Error("Address-balance evidence does not cover the transaction");
189
+ }
190
+ const epoch = value.observedEpoch === undefined ? undefined : BigInt(value.observedEpoch);
191
+ for (const reserved of value.reservations) {
192
+ if (epoch === undefined || (epoch !== BigInt(reserved.epoch) && epoch !== BigInt(reserved.epoch) + 1n))
193
+ throw new Error("Coin reservation epoch expired");
194
+ }
195
+ const expiry = value.expiration;
196
+ if (value.gasMode === "address_balance" && expiry.kind !== "valid_during")
197
+ throw new Error("Missing funding epoch window");
198
+ if (expiry.kind === "valid_during" && (expiry.chain !== value.chainIdentifier || BigInt(expiry.maxEpoch) < BigInt(expiry.minEpoch) ||
199
+ BigInt(expiry.maxEpoch) > BigInt(expiry.minEpoch) + 1n))
200
+ throw new Error("Invalid funding epoch window");
201
+ if (expiry.kind === "valid_during" && (epoch === undefined || epoch < BigInt(expiry.minEpoch) || epoch > BigInt(expiry.maxEpoch)))
202
+ throw new Error("Funding epoch window expired");
203
+ if (expiry.kind === "epoch" && (epoch === undefined || epoch > BigInt(expiry.epoch)))
204
+ throw new Error("Transaction epoch expired");
205
+ }
206
+ export async function readTransactionFunding(description, source) {
207
+ const addressBalances = [];
208
+ let observedEpoch;
209
+ try {
210
+ if (description.expiration.kind !== "none" || description.reservations.length) {
211
+ if (!source)
212
+ throw new Error("Funding source unavailable");
213
+ observedEpoch = raw.parse((await source.getCurrentSystemState()).systemState.epoch);
214
+ }
215
+ for (const [coinType, required] of addressFundingRequirements(description)) {
216
+ if (!source)
217
+ throw new Error("Funding source unavailable");
218
+ const response = (await source.getBalance({ owner: description.sender, coinType })).balance;
219
+ if (normalizeCoinType(response.coinType) !== coinType || !/^(0|[1-9][0-9]*)$/.test(response.addressBalance))
220
+ throw new Error("Invalid address-balance response");
221
+ if (BigInt(response.addressBalance) < required)
222
+ throw new TransactionFundingError(coinType === SUI_TYPE_ARG && !description.withdrawals.some((item) => item.coinType === coinType) ? "insufficient_gas" : "insufficient_balance", "The reviewed account's address balance does not cover the transaction's withdrawal and gas reservations. Nothing was submitted.");
223
+ addressBalances.push({ owner: description.sender, coinType, balanceRaw: response.addressBalance, requiredRaw: required.toString() });
224
+ }
225
+ }
226
+ catch (error) {
227
+ if (error instanceof TransactionFundingError)
228
+ throw error;
229
+ throw new TransactionFundingError("unavailable", "Transaction funding could not be verified from mainnet balance and epoch reads. Nothing was submitted.");
230
+ }
231
+ return transactionFundingEvidenceSchema.parse({ ...description, ...(observedEpoch === undefined ? {} : { observedEpoch }), addressBalances });
232
+ }
233
+ export function assertTransactionFundingMatches(description, evidence) {
234
+ if (!isDeepStrictEqual(description, transactionFundingDescriptionSchema.parse(Object.fromEntries(Object.keys(transactionFundingDescriptionSchema.shape).map((key) => [key, evidence[key]])))))
235
+ throw new Error("Transaction funding evidence does not match stored transaction material");
236
+ }
237
+ export function transactionFundingSummary(funding) {
238
+ transactionFundingEvidenceSchema.parse(funding);
239
+ const gas = funding.gasMode === "address_balance" ? "the reviewed account's SUI address balance"
240
+ : funding.gasMode === "coin_objects" ? "the reviewed account's SUI coin objects"
241
+ : funding.gasObjectIds.length ? "an address-balance reservation and SUI coin objects belonging to the reviewed account"
242
+ : "the reviewed account's SUI address-balance reservation";
243
+ return `Gas payment uses ${gas}. ${funding.withdrawals.length ? "Transaction inputs also reserve funds from the reviewed account's address balances." : "There are no explicit address-balance input withdrawals."} These are review-time funding facts, not guaranteed execution.`;
244
+ }