@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,1639 @@
1
+ import { COIN_METADATA_CACHE_TTL_MS, DEEPBOOK_SCALAR_UNIT_SOURCE, assertValidDecimals, decimalsFromScalar, parseDisplayAmountToRaw, normalizeCoinType } from "./coinMetadata.js";
2
+ import { createDeepBookReadClient } from "./deepbookRawQuoteClient.js";
3
+ import { DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL, DeepbookOfficialIndexerSourceError, deepbookOfficialIndexerIntervalDurationMs, parseDeepbookOfficialIndexerInterval, selectDeepbookOfficialIndexerCanonicalUsdcPools } from "./deepbookOfficialIndexerSource.js";
4
+ import { DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE, DEEPBOOK_SDK_SIMULATION_SOURCE_BASE, DEEPBOOK_SOURCE_FIELD_VALUES } from "./deepbookSourceOwners.js";
5
+ import { deepbookUnitForCoinType, canonicalDeepbookSymbol, getDeepbookCoinEntryBySymbol, getKnownPool, invalidDeepbookScalar, listDeepbookTokenRegistry, PINNED_DEEPBOOK_COINS, resolveDeepbookPoolForSymbols } from "./deepbookRegistry.js";
6
+ import { sumRawAmounts } from "./amounts.js";
7
+ import { buildUsdSettlementAssetGroup, commonAssetGroupDecimals, formatSettlementAssetRawAmount, normalizeSettlementDenomination } from "../evidence/settlementFamilies.js";
8
+ import { assertDeepbookDisplayBalances, assertPositiveInteger, assertValidDeepbookMidPrice, assertValidDeepbookQuote, deepbookAccountInventorySource, deepbookDisplayQuantitySemantics, deepbookMidPriceSemantics, deepbookUsdcPriceHistoryPairFromOfficialPool, deepbookUsdcPriceHistoryQuantitySemantics, deepbookUsdcPriceHistoryResponseSummary, deepbookQuoteQuantitySemantics, normalizeManagerAddresses, normalizeOptionalManagerAddress, parseQuoteDisplayAmount, parseRawAmount, toDeepbookAccountSummary, toDeepbookDisplayQuoteFromRaw } from "./deepbookReadHelpers.js";
9
+ import { classifyWalletBalance, unavailableUnit, unitFromDeepbook, unitFromMetadataRecord, walletBalanceQuantitySemantics, withResolvedUnit, withUnavailableUnit } from "./walletReadHelpers.js";
10
+ import { deepbookAccountInventoryUserAnswerUse, deepbookMidPriceUserAnswerUse, deepbookOrderbookUserAnswerUse, deepbookUsdcPriceAtTimeUserAnswerUse, deepbookUsdcPriceHistoryUserAnswerUse, deepbookQuoteUserAnswerUse, intentEvidenceUserAnswerUse, settlementAssetGroupParityUserAnswerUse, walletBalanceUserAnswerUse, walletClassificationUserAnswerUse } from "./readResponseGuidance.js";
11
+ import { intentEvidenceQuantitySemantics, intentEvidenceResponseEvidence, intentEvidenceResponseSummary, intentEvidenceSettlementAssetCoverageBoundary, intentEvidenceSupportedClaims, isIntentEvidenceTargetAssetSelectionSource, isSupportedIntentEvidenceKind } from "./intentEvidenceResponseFormatting.js";
12
+ import { roundPriceInReferenceAsset, settlementAssetGroupParityQuantitySemantics, settlementAssetGroupParityResponseSummary, settlementAssetGroupParityStatistics } from "./settlementParityFormatting.js";
13
+ import { DEEPBOOK_MID_PRICE_DIRECTION, DEEPBOOK_MID_PRICE_PRECISION, DEEPBOOK_MID_PRICE_TYPE, DEEPBOOK_RAW_QUOTE_QUANTITY_KIND, DEEPBOOK_USDC_PRICE_HISTORY_UNSUPPORTED_CLAIMS, DEFAULT_DEEPBOOK_SIMULATION_SENDER, INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES, MAX_DEEPBOOK_ACCOUNT_OPEN_ORDER_IDS, MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS, MAX_DEEPBOOK_ORDERBOOK_TICKS, MAX_WALLET_BALANCE_SCAN_PAGES, NOT_INSPECTED_ASSET_CLASSES, ReadServiceCacheError, ReadServiceInputError } from "./readServiceTypes.js";
14
+ export * from "./readServiceTypes.js";
15
+ export { listDeepbookTokenRegistry } from "./deepbookRegistry.js";
16
+ const DEFAULT_DEEPBOOK_USDC_PRICE_AT_TIME_MAX_DISTANCE_MINUTES = 360;
17
+ // Account inventory object scan: pages of listOwnedObjects to walk (50 objects per
18
+ // page) and the max NFTs to surface. A coin-heavy account may truncate before all
19
+ // objects are seen; the snapshot flags that rather than paging unbounded.
20
+ const ACCOUNT_OBJECT_SCAN_PAGES = 6;
21
+ const ACCOUNT_NFT_LIMIT = 60;
22
+ const DEEPBOOK_USDC_PRICE_HISTORY_RESPONSE_SUMMARY = {
23
+ ...deepbookUsdcPriceHistoryResponseSummary()
24
+ };
25
+ function deepbookUsdcPriceHistorySelector(input) {
26
+ const selectors = [
27
+ input.poolName === undefined ? undefined : { kind: "pool_name", value: input.poolName.trim().toUpperCase() },
28
+ input.assetSymbol === undefined
29
+ ? undefined
30
+ : { kind: "asset_symbol", value: input.assetSymbol.trim().toUpperCase() },
31
+ input.coinType === undefined
32
+ ? undefined
33
+ : { kind: "coin_type", value: normalizedHistoryCoinType(input.coinType) }
34
+ ].filter((selector) => selector !== undefined && selector.value.length > 0);
35
+ if (selectors.length !== 1) {
36
+ throw new ReadServiceInputError("input_invalid", "Provide exactly one DeepBook USDC history selector: poolName, assetSymbol, or coinType", {
37
+ fields: ["poolName", "assetSymbol", "coinType"],
38
+ providedSelectorCount: selectors.length
39
+ });
40
+ }
41
+ return selectors[0];
42
+ }
43
+ function normalizedHistoryCoinType(value) {
44
+ try {
45
+ return normalizeCoinType(value);
46
+ }
47
+ catch {
48
+ throw new ReadServiceInputError("input_invalid", "coinType must be a valid Sui coin type", {
49
+ field: "coinType",
50
+ value
51
+ });
52
+ }
53
+ }
54
+ function parseDeepbookUsdcPriceHistoryInterval(value) {
55
+ if (value === undefined) {
56
+ return DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL;
57
+ }
58
+ if (typeof value !== "string") {
59
+ throw new ReadServiceInputError("input_invalid", "interval must be a supported DeepBook official Indexer interval", {
60
+ field: "interval",
61
+ value
62
+ });
63
+ }
64
+ try {
65
+ return parseDeepbookOfficialIndexerInterval(value);
66
+ }
67
+ catch (error) {
68
+ if (error instanceof DeepbookOfficialIndexerSourceError && error.reason === "invalid_payload") {
69
+ throw new ReadServiceInputError("input_invalid", "interval must be a supported DeepBook official Indexer interval", {
70
+ field: "interval",
71
+ value
72
+ });
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ function deepbookUsdcPriceHistoryRange(start, end, interval) {
78
+ const startDate = parseUtcIsoHistoryDate(start, "start");
79
+ const endDate = parseUtcIsoHistoryDate(end, "end");
80
+ const durationMs = endDate.getTime() - startDate.getTime();
81
+ if (durationMs <= 0) {
82
+ throw new ReadServiceInputError("input_invalid", "end must be after start for DeepBook USDC history", {
83
+ start,
84
+ end
85
+ });
86
+ }
87
+ const selectedInterval = parseDeepbookUsdcPriceHistoryInterval(interval);
88
+ const intervalDurationMs = deepbookOfficialIndexerIntervalDurationMs(selectedInterval);
89
+ return {
90
+ start: startDate.toISOString(),
91
+ end: endDate.toISOString(),
92
+ timeZone: "UTC",
93
+ interval: selectedInterval,
94
+ intervalDurationMs,
95
+ maxBars: MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS,
96
+ requestedCandleSlots: Math.ceil(durationMs / intervalDurationMs)
97
+ };
98
+ }
99
+ function deepbookUsdcPriceAtTimeTarget(input) {
100
+ const target = parseUtcIsoHistoryDate(input.targetTime, "targetTime");
101
+ const maxDistanceMinutes = input.maxDistanceMinutes ?? DEFAULT_DEEPBOOK_USDC_PRICE_AT_TIME_MAX_DISTANCE_MINUTES;
102
+ if (!Number.isInteger(maxDistanceMinutes) || maxDistanceMinutes <= 0) {
103
+ throw new ReadServiceInputError("input_invalid", "maxDistanceMinutes must be a positive integer", {
104
+ field: "maxDistanceMinutes",
105
+ value: input.maxDistanceMinutes
106
+ });
107
+ }
108
+ const interval = parseDeepbookUsdcPriceHistoryInterval(input.interval);
109
+ const intervalDurationMs = deepbookOfficialIndexerIntervalDurationMs(interval);
110
+ const intervalMinutes = intervalDurationMs / 60_000;
111
+ const maxSearchMinutes = Math.floor((MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS * intervalMinutes -
112
+ intervalMinutes) /
113
+ 2);
114
+ if (maxDistanceMinutes > maxSearchMinutes) {
115
+ throw new ReadServiceInputError("input_invalid", "maxDistanceMinutes exceeds the supported DeepBook USDC search window", {
116
+ field: "maxDistanceMinutes",
117
+ maxDistanceMinutes,
118
+ maxSupportedMinutes: maxSearchMinutes
119
+ });
120
+ }
121
+ const targetMs = target.getTime();
122
+ const start = new Date(targetMs - maxDistanceMinutes * 60 * 1000);
123
+ const end = new Date(targetMs + maxDistanceMinutes * 60 * 1000 + intervalDurationMs);
124
+ return {
125
+ targetTime: target.toISOString(),
126
+ searchWindow: {
127
+ start: start.toISOString(),
128
+ end: end.toISOString(),
129
+ maxDistanceMinutes
130
+ }
131
+ };
132
+ }
133
+ function parseUtcIsoHistoryDate(value, field) {
134
+ const timestamp = Date.parse(value);
135
+ if (!Number.isFinite(timestamp)) {
136
+ throw new ReadServiceInputError("input_invalid", `${field} must be an ISO UTC timestamp`, { field, value });
137
+ }
138
+ const date = new Date(timestamp);
139
+ if (date.toISOString() !== value) {
140
+ throw new ReadServiceInputError("input_invalid", `${field} must be a canonical ISO UTC timestamp`, {
141
+ field,
142
+ value
143
+ });
144
+ }
145
+ return date;
146
+ }
147
+ function deepbookUsdcPriceHistoryMatchingPairs(usdcPools, selector) {
148
+ switch (selector.kind) {
149
+ case "pool_name":
150
+ return usdcPools.filter((pool) => pool.pool_name.toUpperCase() === selector.value);
151
+ case "asset_symbol":
152
+ return usdcPools.filter((pool) => pool.base_asset_symbol.toUpperCase() === selector.value);
153
+ case "coin_type":
154
+ return usdcPools.filter((pool) => normalizeCoinType(pool.base_asset_id) === selector.value);
155
+ }
156
+ }
157
+ function deepbookUsdcPriceHistorySource(input) {
158
+ return {
159
+ kind: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.kind,
160
+ baseUrl: input.poolSource.baseUrl,
161
+ sourceStatement: input.candleSource.sourceStatement,
162
+ poolList: {
163
+ url: input.poolSource.url,
164
+ fetchedAt: input.poolSource.fetchedAt
165
+ },
166
+ candles: {
167
+ url: input.candleSource.url,
168
+ fetchedAt: input.candleSource.fetchedAt,
169
+ poolName: input.candleSource.poolName ?? "",
170
+ interval: input.candleSource.interval ?? DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL,
171
+ startTimeMs: input.candleSource.startTimeMs ?? 0,
172
+ endTimeMs: input.candleSource.endTimeMs ?? 0,
173
+ limit: input.candleSource.limit ?? MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS
174
+ },
175
+ chainRecomputedBySuiMcp: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.chainRecomputedBySuiMcp
176
+ };
177
+ }
178
+ function deepbookOfficialSourceUnavailableReason(error, phase) {
179
+ if (error instanceof DeepbookOfficialIndexerSourceError && error.reason === "invalid_payload") {
180
+ return "official_indexer_invalid_payload";
181
+ }
182
+ return phase === "pools" ? "pool_list_unavailable" : "candle_fetch_failed";
183
+ }
184
+ function deepbookUsdcPriceAtTimeMatch(input) {
185
+ const targetMs = Date.parse(input.target.targetTime);
186
+ const exact = input.bars.find((candle) => {
187
+ const startMs = Date.parse(candle.start);
188
+ const endMs = Date.parse(candle.end);
189
+ return targetMs >= startMs && targetMs < endMs;
190
+ });
191
+ if (exact !== undefined) {
192
+ return {
193
+ candle: exact,
194
+ match: deepbookUsdcPriceAtTimeMatchFromBar({
195
+ kind: "exact_bucket",
196
+ distanceMinutes: 0,
197
+ pair: input.pair,
198
+ candle: exact
199
+ })
200
+ };
201
+ }
202
+ const candidates = input.bars
203
+ .flatMap((candle) => {
204
+ const startMs = Date.parse(candle.start);
205
+ const endMs = Date.parse(candle.end);
206
+ if (endMs <= targetMs) {
207
+ return [
208
+ {
209
+ candle,
210
+ kind: "nearest_before",
211
+ distanceMinutes: (targetMs - endMs) / 60_000
212
+ }
213
+ ];
214
+ }
215
+ if (startMs > targetMs) {
216
+ return [
217
+ {
218
+ candle,
219
+ kind: "nearest_after",
220
+ distanceMinutes: (startMs - targetMs) / 60_000
221
+ }
222
+ ];
223
+ }
224
+ return [];
225
+ })
226
+ .filter((candidate) => candidate.distanceMinutes <= input.target.searchWindow.maxDistanceMinutes)
227
+ .sort((left, right) => {
228
+ const byDistance = left.distanceMinutes - right.distanceMinutes;
229
+ if (byDistance !== 0) {
230
+ return byDistance;
231
+ }
232
+ if (left.kind !== right.kind) {
233
+ return left.kind === "nearest_before" ? -1 : 1;
234
+ }
235
+ return Date.parse(left.candle.start) - Date.parse(right.candle.start);
236
+ });
237
+ const best = candidates[0];
238
+ if (best === undefined) {
239
+ return undefined;
240
+ }
241
+ return {
242
+ candle: best.candle,
243
+ match: deepbookUsdcPriceAtTimeMatchFromBar({
244
+ kind: best.kind,
245
+ distanceMinutes: best.distanceMinutes,
246
+ pair: input.pair,
247
+ candle: best.candle
248
+ })
249
+ };
250
+ }
251
+ function deepbookUsdcPriceAtTimeMatchFromBar(input) {
252
+ return {
253
+ kind: input.kind,
254
+ distanceMinutes: Number(input.distanceMinutes.toFixed(6)),
255
+ representativePrice: {
256
+ field: "matchedCandle.close",
257
+ value: input.candle.close,
258
+ quoteAsset: "USDC",
259
+ baseAssetSymbol: input.pair.baseAsset.symbol,
260
+ priceConvention: "USDC_PER_BASE"
261
+ }
262
+ };
263
+ }
264
+ export class SuiReadService {
265
+ #client;
266
+ #network;
267
+ #chainIdentifier;
268
+ #coinMetadataCache;
269
+ #now;
270
+ #deepbookFactory;
271
+ #coinMetadataTtlMs;
272
+ #deepbookCoins;
273
+ #deepbookOfficialIndexerSource;
274
+ constructor(options) {
275
+ this.#client = options.client;
276
+ this.#network = options.network;
277
+ this.#chainIdentifier = options.chainIdentifier;
278
+ this.#coinMetadataCache = options.coinMetadataCache;
279
+ this.#now = options.now ?? (() => new Date());
280
+ this.#coinMetadataTtlMs = options.coinMetadataTtlMs ?? COIN_METADATA_CACHE_TTL_MS;
281
+ this.#deepbookCoins = options.deepbookCoins ?? PINNED_DEEPBOOK_COINS;
282
+ this.#deepbookFactory =
283
+ options.deepbookFactory ??
284
+ ((simulationSender, factoryOptions) => createDeepBookReadClient({
285
+ client: options.client,
286
+ simulationSender,
287
+ network: this.#network,
288
+ ...(factoryOptions?.balanceManagers === undefined
289
+ ? {}
290
+ : { balanceManagers: factoryOptions.balanceManagers })
291
+ }));
292
+ this.#deepbookOfficialIndexerSource = options.deepbookOfficialIndexerSource;
293
+ }
294
+ async summarizeWalletAssets(input) {
295
+ const options = { owner: input.account };
296
+ if (input.cursor !== undefined) {
297
+ options.cursor = input.cursor;
298
+ }
299
+ const result = await this.#client.core.listBalances(options);
300
+ const balances = [];
301
+ for (const balance of result.balances) {
302
+ balances.push(await this.#withUnit(balance));
303
+ }
304
+ return {
305
+ status: "ok",
306
+ account: input.account,
307
+ fetchedAt: this.#fetchedAt(),
308
+ userAnswerUse: walletBalanceUserAnswerUse(),
309
+ quantitySemantics: walletBalanceQuantitySemantics(),
310
+ source: {
311
+ sdk: "@mysten/sui",
312
+ transport: "grpc",
313
+ method: "client.core.listBalances"
314
+ },
315
+ balances,
316
+ hasNextPage: result.hasNextPage,
317
+ cursor: result.cursor
318
+ };
319
+ }
320
+ async classifyWalletAssets(input) {
321
+ const summary = await this.summarizeWalletAssets(input);
322
+ return {
323
+ status: "ok",
324
+ account: summary.account,
325
+ fetchedAt: summary.fetchedAt,
326
+ userAnswerUse: walletClassificationUserAnswerUse(),
327
+ quantitySemantics: summary.quantitySemantics,
328
+ source: summary.source,
329
+ classifiedAssets: summary.balances.map((balance) => classifyWalletBalance(balance, this.#deepbookCoins)),
330
+ uninspectedAssetClasses: NOT_INSPECTED_ASSET_CLASSES.map((assetClass) => ({ ...assetClass })),
331
+ hasNextPage: summary.hasNextPage,
332
+ cursor: summary.cursor
333
+ };
334
+ }
335
+ // The public account snapshot for the account page: the SuiNS name, coin
336
+ // balances (with the held-as split), owned NFTs, and other owned objects grouped
337
+ // by type. Each part is read independently so a missing name or object scan never
338
+ // blocks the balances.
339
+ async summarizeAccountInventory(input) {
340
+ const [walletSummary, name, owned] = await Promise.all([
341
+ this.summarizeWalletAssets(input),
342
+ this.#resolveAccountName(input.account),
343
+ this.#scanOwnedObjects(input.account)
344
+ ]);
345
+ return {
346
+ status: "ok",
347
+ account: input.account,
348
+ fetchedAt: this.#fetchedAt(),
349
+ name,
350
+ // Drop coins whose total balance is zero — an emptied coin object or
351
+ // accumulator residue is not a holding worth listing on the account page.
352
+ balances: walletSummary.balances.filter((balance) => {
353
+ try {
354
+ return BigInt(balance.balance) !== 0n;
355
+ }
356
+ catch {
357
+ return true;
358
+ }
359
+ }),
360
+ nfts: owned.nfts,
361
+ objectGroups: owned.objectGroups,
362
+ scannedObjects: owned.scanned,
363
+ objectsTruncated: owned.truncated
364
+ };
365
+ }
366
+ // The account's primary SuiNS name, or null when it has none (the lookup throws
367
+ // NOT_FOUND) or the call fails — a name is convenience, never gating.
368
+ async #resolveAccountName(account) {
369
+ const resolve = this.#client.core.defaultNameServiceName;
370
+ if (!resolve) {
371
+ return null;
372
+ }
373
+ try {
374
+ const response = await resolve.call(this.#client.core, { address: account });
375
+ return response.data.name ?? null;
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ // Enumerate owned objects (capped) and split them: Coin objects belong to the
382
+ // balance view, objects carrying a Display name/image are NFTs, and the rest are
383
+ // grouped by Move type. listOwnedObjects returns all three intermixed, so a
384
+ // coin-heavy account may truncate before reaching every NFT.
385
+ async #scanOwnedObjects(account) {
386
+ const nfts = [];
387
+ const groups = new Map();
388
+ const core = this.#client.core;
389
+ const listOwnedObjects = core.listOwnedObjects;
390
+ if (!listOwnedObjects) {
391
+ return { nfts, objectGroups: [], scanned: 0, truncated: false };
392
+ }
393
+ let cursor;
394
+ let scanned = 0;
395
+ let truncated = false;
396
+ for (let page = 0; page < ACCOUNT_OBJECT_SCAN_PAGES; page += 1) {
397
+ const response = await listOwnedObjects.call(core, {
398
+ owner: account,
399
+ include: { display: true },
400
+ ...(cursor === undefined ? {} : { cursor })
401
+ });
402
+ for (const object of response.objects ?? []) {
403
+ scanned += 1;
404
+ const type = object.type ?? "";
405
+ if (type.includes("::coin::Coin<")) {
406
+ continue;
407
+ }
408
+ const output = object.display?.output;
409
+ const name = typeof output?.name === "string" ? output.name : undefined;
410
+ const imageUrl = typeof output?.image_url === "string" ? output.image_url : undefined;
411
+ if (name !== undefined || imageUrl !== undefined) {
412
+ if (nfts.length < ACCOUNT_NFT_LIMIT) {
413
+ nfts.push({ objectId: object.objectId, type, name, imageUrl });
414
+ }
415
+ }
416
+ else {
417
+ groups.set(type, (groups.get(type) ?? 0) + 1);
418
+ }
419
+ }
420
+ if (!response.hasNextPage) {
421
+ break;
422
+ }
423
+ cursor = response.cursor ?? undefined;
424
+ if (page === ACCOUNT_OBJECT_SCAN_PAGES - 1) {
425
+ truncated = true;
426
+ }
427
+ }
428
+ const objectGroups = [...groups.entries()]
429
+ .map(([type, count]) => ({ type, count }))
430
+ .sort((left, right) => right.count - left.count || left.type.localeCompare(right.type));
431
+ return { nfts, objectGroups, scanned, truncated };
432
+ }
433
+ listSettlementAssetGroups() {
434
+ return {
435
+ status: "ok",
436
+ fetchedAt: this.#fetchedAt(),
437
+ assetGroups: [buildUsdSettlementAssetGroup(this.#deepbookCoins)]
438
+ };
439
+ }
440
+ async summarizeSettlementAssetGroupParity(input) {
441
+ let denomination;
442
+ try {
443
+ denomination = normalizeSettlementDenomination(input.denomination);
444
+ }
445
+ catch {
446
+ throw new ReadServiceInputError("input_invalid", "Unsupported settlement denomination", {
447
+ field: "denomination",
448
+ value: input.denomination,
449
+ supportedAliases: buildUsdSettlementAssetGroup(this.#deepbookCoins).aliases
450
+ });
451
+ }
452
+ const assetGroup = buildUsdSettlementAssetGroup(this.#deepbookCoins);
453
+ if (assetGroup.includedAssets.length === 0) {
454
+ throw new ReadServiceInputError("registry_miss", "No pinned USD-denominated settlement assets are available", {
455
+ assetGroupId: assetGroup.id
456
+ });
457
+ }
458
+ const referenceAsset = this.#resolveSettlementAssetGroupSymbol(input.referenceAssetSymbol ?? "USDC", assetGroup, "referenceAssetSymbol");
459
+ const deepbook = this.#deepbookFactory(input.simulationSender);
460
+ const assets = await Promise.all(assetGroup.includedAssets.map((asset) => this.#settlementAssetGroupParityAsset(asset, referenceAsset, deepbook)));
461
+ const samples = assets.filter((asset) => asset.status === "reference_asset" || asset.status === "measured");
462
+ if (samples.length === 0) {
463
+ throw new ReadServiceInputError("registry_miss", "No parity samples are available for the settlement asset group", {
464
+ assetGroupId: assetGroup.id,
465
+ referenceAssetSymbol: referenceAsset.symbol
466
+ });
467
+ }
468
+ const statistics = settlementAssetGroupParityStatistics(samples, assets.length - samples.length);
469
+ return {
470
+ status: "ok",
471
+ fetchedAt: this.#fetchedAt(),
472
+ denomination,
473
+ assetGroupId: assetGroup.id,
474
+ userAnswerUse: settlementAssetGroupParityUserAnswerUse(),
475
+ referenceAsset: {
476
+ ...referenceAsset,
477
+ role: "measurement_reference_not_settlement_choice"
478
+ },
479
+ quantitySemantics: settlementAssetGroupParityQuantitySemantics(),
480
+ evidenceSources: {
481
+ settlementAssetGroup: assetGroup.evidenceSources,
482
+ midPrice: {
483
+ ...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
484
+ method: "midPrice",
485
+ precision: DEEPBOOK_MID_PRICE_PRECISION
486
+ }
487
+ },
488
+ assets,
489
+ statistics,
490
+ responseSummary: settlementAssetGroupParityResponseSummary({
491
+ assetGroupId: assetGroup.id,
492
+ referenceAssetSymbol: referenceAsset.symbol,
493
+ statistics
494
+ }),
495
+ unsupportedClaims: [
496
+ "settlement_token_selection",
497
+ "fiat_usd_cash_out",
498
+ "payment_execution_readiness",
499
+ "route_recommendation",
500
+ "best_route",
501
+ "transaction_building",
502
+ "signing_readiness",
503
+ "profit_or_pnl",
504
+ "cost_basis"
505
+ ]
506
+ };
507
+ }
508
+ async previewIntentEvidence(input) {
509
+ if (!isSupportedIntentEvidenceKind(input.intentKind)) {
510
+ throw new ReadServiceInputError("input_invalid", "Unsupported intentKind", {
511
+ field: "intentKind",
512
+ value: input.intentKind
513
+ });
514
+ }
515
+ if (input.intentKind === "cover_payment_like_amount" && input.requiredDisplayAmount === undefined) {
516
+ throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount is required for cover_payment_like_amount", {
517
+ field: "requiredDisplayAmount",
518
+ intentKind: input.intentKind
519
+ });
520
+ }
521
+ if (input.targetAssetSelectionSource !== undefined &&
522
+ !isIntentEvidenceTargetAssetSelectionSource(input.targetAssetSelectionSource)) {
523
+ throw new ReadServiceInputError("input_invalid", "Unsupported targetAssetSelectionSource", {
524
+ field: "targetAssetSelectionSource",
525
+ value: input.targetAssetSelectionSource,
526
+ supportedValues: [...INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES]
527
+ });
528
+ }
529
+ if (input.intentKind === "summarize_settlement_asset_group_balance") {
530
+ if (input.requiredDisplayAmount !== undefined) {
531
+ throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount is only supported for cover_payment_like_amount", {
532
+ field: "requiredDisplayAmount",
533
+ intentKind: input.intentKind
534
+ });
535
+ }
536
+ if (input.targetAssetSymbol !== undefined) {
537
+ throw new ReadServiceInputError("input_invalid", "targetAssetSymbol is only supported for cover_payment_like_amount", {
538
+ field: "targetAssetSymbol",
539
+ intentKind: input.intentKind
540
+ });
541
+ }
542
+ if (input.targetAssetSelectionSource !== undefined) {
543
+ throw new ReadServiceInputError("input_invalid", "targetAssetSelectionSource is only supported for cover_payment_like_amount", {
544
+ field: "targetAssetSelectionSource",
545
+ intentKind: input.intentKind
546
+ });
547
+ }
548
+ if (input.acceptedSourceAssetSymbols !== undefined) {
549
+ throw new ReadServiceInputError("input_invalid", "acceptedSourceAssetSymbols is only supported for cover_payment_like_amount", {
550
+ field: "acceptedSourceAssetSymbols",
551
+ intentKind: input.intentKind
552
+ });
553
+ }
554
+ }
555
+ if (input.intentKind === "cover_payment_like_amount") {
556
+ if (input.targetAssetSymbol !== undefined && input.targetAssetSelectionSource === undefined) {
557
+ throw new ReadServiceInputError("input_invalid", "targetAssetSelectionSource is required when targetAssetSymbol is supplied", {
558
+ field: "targetAssetSelectionSource",
559
+ requiredWith: "targetAssetSymbol",
560
+ supportedValues: [...INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES]
561
+ });
562
+ }
563
+ if (input.targetAssetSymbol === undefined && input.targetAssetSelectionSource !== undefined) {
564
+ throw new ReadServiceInputError("input_invalid", "targetAssetSymbol is required when targetAssetSelectionSource is supplied", {
565
+ field: "targetAssetSymbol",
566
+ requiredWith: "targetAssetSelectionSource"
567
+ });
568
+ }
569
+ }
570
+ let denomination;
571
+ try {
572
+ denomination = normalizeSettlementDenomination(input.denomination);
573
+ }
574
+ catch {
575
+ throw new ReadServiceInputError("input_invalid", "Unsupported settlement denomination", {
576
+ field: "denomination",
577
+ value: input.denomination,
578
+ supportedAliases: buildUsdSettlementAssetGroup(this.#deepbookCoins).aliases
579
+ });
580
+ }
581
+ const assetGroup = buildUsdSettlementAssetGroup(this.#deepbookCoins);
582
+ if (assetGroup.includedAssets.length === 0) {
583
+ throw new ReadServiceInputError("registry_miss", "No pinned USD-denominated settlement assets are available", {
584
+ assetGroupId: assetGroup.id
585
+ });
586
+ }
587
+ const targetAsset = input.intentKind !== "cover_payment_like_amount" || input.targetAssetSymbol === undefined
588
+ ? undefined
589
+ : this.#resolveSettlementAssetGroupSymbol(input.targetAssetSymbol, assetGroup, "targetAssetSymbol");
590
+ const targetAssetSelectionSource = targetAsset === undefined ? undefined : input.targetAssetSelectionSource;
591
+ const acceptedSourceSymbols = input.intentKind !== "cover_payment_like_amount" || input.acceptedSourceAssetSymbols === undefined
592
+ ? undefined
593
+ : input.acceptedSourceAssetSymbols.map((symbol, index) => this.#resolveSettlementAssetGroupSymbol(symbol, assetGroup, `acceptedSourceAssetSymbols[${index}]`).symbol);
594
+ const acceptedSourceSet = acceptedSourceSymbols === undefined ? undefined : new Set(acceptedSourceSymbols);
595
+ const scan = await this.#scanWalletAssetClassificationPages(input.account);
596
+ const balances = this.#intentEvidenceAssetGroupBalances(assetGroup, scan.classifiedAssets);
597
+ const commonDecimals = commonAssetGroupDecimals(assetGroup.includedAssets);
598
+ const aggregate = this.#intentEvidenceAggregate({
599
+ requiredDisplayAmount: input.requiredDisplayAmount,
600
+ balances,
601
+ commonDecimals,
602
+ blockedReason: scan.blockedReason
603
+ });
604
+ const settlementAssetCoverage = this.#intentEvidenceSettlementAssetCoverage(aggregate);
605
+ const requiredDisplayAmount = input.requiredDisplayAmount;
606
+ let selectedTarget;
607
+ if (targetAsset !== undefined) {
608
+ if (requiredDisplayAmount === undefined) {
609
+ throw new Error("target settlement evidence requires requiredDisplayAmount");
610
+ }
611
+ if (targetAssetSelectionSource === undefined) {
612
+ throw new Error("target settlement evidence requires user selection provenance");
613
+ }
614
+ if (scan.blockedReason === undefined) {
615
+ selectedTarget = this.#intentEvidenceSelectedTarget({
616
+ targetAsset,
617
+ selectionSource: targetAssetSelectionSource,
618
+ requiredDisplayAmount,
619
+ balances
620
+ });
621
+ }
622
+ }
623
+ const candidateConversions = input.intentKind === "cover_payment_like_amount" && scan.blockedReason === undefined
624
+ ? await this.#intentEvidenceCandidateConversions({
625
+ targetAsset,
626
+ balances,
627
+ acceptedSourceSet
628
+ })
629
+ : [];
630
+ const requiredUserChoices = this.#intentEvidenceRequiredUserChoices(input.intentKind, targetAsset, candidateConversions);
631
+ const responseEvidence = intentEvidenceResponseEvidence(targetAsset, settlementAssetCoverage, candidateConversions);
632
+ const responseSummary = intentEvidenceResponseSummary({
633
+ intentKind: input.intentKind,
634
+ assetGroupId: assetGroup.id,
635
+ settlementAssetCoverage,
636
+ responseEvidenceMode: responseEvidence.mode,
637
+ requiredUserChoices
638
+ });
639
+ const { excludedAssets: _excludedAssets, ...settlementAssetGroup } = assetGroup;
640
+ return {
641
+ status: "ok",
642
+ account: input.account,
643
+ fetchedAt: this.#fetchedAt(),
644
+ userAnswerUse: intentEvidenceUserAnswerUse(settlementAssetCoverage.status, responseEvidence),
645
+ intent: {
646
+ intentKind: input.intentKind,
647
+ denomination,
648
+ ...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount }),
649
+ ...(targetAsset === undefined
650
+ ? {}
651
+ : { targetAssetSymbol: targetAsset.symbol, targetAssetSelectionSource }),
652
+ ...(acceptedSourceSymbols === undefined ? {} : { acceptedSourceAssetSymbols: acceptedSourceSymbols })
653
+ },
654
+ quantitySemantics: intentEvidenceQuantitySemantics(),
655
+ evidenceSources: {
656
+ walletBalances: {
657
+ sdk: "@mysten/sui",
658
+ transport: "grpc",
659
+ method: "client.core.listBalances"
660
+ },
661
+ settlementAssetGroup: assetGroup.evidenceSources,
662
+ quoteEvidence: DEEPBOOK_SOURCE_FIELD_VALUES.pinnedSdkWhenTargetAssetSelected
663
+ },
664
+ settlementAssetGroup,
665
+ balances,
666
+ aggregate,
667
+ settlementAssetCoverage,
668
+ ...(selectedTarget === undefined ? {} : { selectedTarget }),
669
+ candidateConversions,
670
+ blockedReasons: scan.blockedReason === undefined ? [] : [scan.blockedReason],
671
+ responseEvidence,
672
+ responseSummary,
673
+ requiredUserChoices,
674
+ supportedClaims: intentEvidenceSupportedClaims(settlementAssetCoverage, selectedTarget, candidateConversions),
675
+ unsupportedClaims: [
676
+ "settlement_token_selection",
677
+ "fiat_usd_cash_out",
678
+ "gas_reserve_or_fee_readiness",
679
+ "best_route_or_venue_comparison",
680
+ "route_dependent_payment_support",
681
+ "payment_execution_readiness",
682
+ "transaction_building",
683
+ "signing_readiness",
684
+ "profit_or_pnl",
685
+ "cost_basis"
686
+ ],
687
+ uninspectedAssetClasses: scan.uninspectedAssetClasses,
688
+ inspectedBalancePages: scan.inspectedBalancePages,
689
+ inspectedCoinBalanceCount: scan.inspectedCoinBalanceCount
690
+ };
691
+ }
692
+ async inspectDeepbookOrderbook(input) {
693
+ getKnownPool(input.poolKey);
694
+ assertPositiveInteger(input.ticks, "ticks", MAX_DEEPBOOK_ORDERBOOK_TICKS);
695
+ const deepbook = this.#deepbookFactory(input.simulationSender);
696
+ const [midPrice, poolBookParams, level2TicksFromMid] = await Promise.all([
697
+ deepbook.midPrice(input.poolKey),
698
+ deepbook.poolBookParams(input.poolKey),
699
+ deepbook.getLevel2TicksFromMid(input.poolKey, input.ticks)
700
+ ]);
701
+ const checkedMidPrice = assertValidDeepbookMidPrice(input.poolKey, midPrice);
702
+ return {
703
+ status: "ok",
704
+ poolKey: input.poolKey,
705
+ ticks: input.ticks,
706
+ fetchedAt: this.#fetchedAt(),
707
+ userAnswerUse: deepbookOrderbookUserAnswerUse(),
708
+ source: {
709
+ ...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
710
+ methods: ["midPrice", "poolBookParams", "getLevel2TicksFromMid"]
711
+ },
712
+ midPrice: checkedMidPrice,
713
+ poolBookParams,
714
+ level2TicksFromMid
715
+ };
716
+ }
717
+ async getDeepbookMidPrice(input) {
718
+ const pool = getKnownPool(input.poolKey);
719
+ const deepbook = this.#deepbookFactory(input.simulationSender);
720
+ const midPrice = assertValidDeepbookMidPrice(input.poolKey, await deepbook.midPrice(input.poolKey));
721
+ return {
722
+ status: "ok",
723
+ poolKey: input.poolKey,
724
+ base: pool.baseCoin,
725
+ quote: pool.quoteCoin,
726
+ userAnswerUse: deepbookMidPriceUserAnswerUse(),
727
+ priceSemantics: deepbookMidPriceSemantics(),
728
+ price: midPrice,
729
+ priceDirection: DEEPBOOK_MID_PRICE_DIRECTION,
730
+ priceType: DEEPBOOK_MID_PRICE_TYPE,
731
+ fetchedAt: this.#fetchedAt(),
732
+ source: {
733
+ ...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
734
+ method: "midPrice",
735
+ precision: DEEPBOOK_MID_PRICE_PRECISION
736
+ }
737
+ };
738
+ }
739
+ async getDeepbookUsdcPriceHistory(input) {
740
+ const fetchedAt = this.#fetchedAt();
741
+ const selector = deepbookUsdcPriceHistorySelector(input);
742
+ const range = deepbookUsdcPriceHistoryRange(input.start, input.end, input.interval);
743
+ const common = {
744
+ fetchedAt,
745
+ requested: { selector, range },
746
+ userAnswerUse: deepbookUsdcPriceHistoryUserAnswerUse(),
747
+ quantitySemantics: deepbookUsdcPriceHistoryQuantitySemantics(),
748
+ responseSummary: DEEPBOOK_USDC_PRICE_HISTORY_RESPONSE_SUMMARY,
749
+ unsupportedClaims: [...DEEPBOOK_USDC_PRICE_HISTORY_UNSUPPORTED_CLAIMS]
750
+ };
751
+ if (range.requestedCandleSlots > MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS) {
752
+ return {
753
+ status: "unsupported_range",
754
+ ...common,
755
+ reason: "requested_range_exceeds_max_bars"
756
+ };
757
+ }
758
+ if (this.#deepbookOfficialIndexerSource === undefined) {
759
+ return {
760
+ status: "source_unavailable",
761
+ ...common,
762
+ reason: "official_indexer_not_configured"
763
+ };
764
+ }
765
+ let poolResult;
766
+ try {
767
+ poolResult = await this.#deepbookOfficialIndexerSource.fetchPools();
768
+ }
769
+ catch (error) {
770
+ return {
771
+ status: "source_unavailable",
772
+ ...common,
773
+ reason: deepbookOfficialSourceUnavailableReason(error, "pools")
774
+ };
775
+ }
776
+ const usdcPools = selectDeepbookOfficialIndexerCanonicalUsdcPools(poolResult.pools);
777
+ const matchingPools = deepbookUsdcPriceHistoryMatchingPairs(usdcPools, selector);
778
+ if (matchingPools.length !== 1) {
779
+ return {
780
+ status: "unsupported_pair",
781
+ ...common,
782
+ reason: matchingPools.length === 0
783
+ ? "selector_not_in_official_indexer"
784
+ : "selector_resolves_to_multiple_usdc_pools",
785
+ matchingPoolNames: matchingPools.map((pool) => pool.pool_name),
786
+ availablePoolNames: usdcPools.map((pool) => pool.pool_name)
787
+ };
788
+ }
789
+ const pool = matchingPools[0];
790
+ const outputPair = deepbookUsdcPriceHistoryPairFromOfficialPool(pool);
791
+ let candleResult;
792
+ try {
793
+ candleResult = await this.#deepbookOfficialIndexerSource.fetchCandles({
794
+ poolName: pool.pool_name,
795
+ interval: range.interval,
796
+ startTimeMs: Date.parse(range.start),
797
+ endTimeMs: Date.parse(range.end),
798
+ limit: MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS
799
+ });
800
+ }
801
+ catch (error) {
802
+ return {
803
+ status: "source_unavailable",
804
+ ...common,
805
+ reason: deepbookOfficialSourceUnavailableReason(error, "candles"),
806
+ pair: outputPair,
807
+ source: {
808
+ kind: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.kind,
809
+ baseUrl: poolResult.source.baseUrl,
810
+ sourceStatement: poolResult.source.sourceStatement,
811
+ poolList: {
812
+ url: poolResult.source.url,
813
+ fetchedAt: poolResult.source.fetchedAt
814
+ },
815
+ chainRecomputedBySuiMcp: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.chainRecomputedBySuiMcp
816
+ }
817
+ };
818
+ }
819
+ const source = deepbookUsdcPriceHistorySource({
820
+ poolSource: poolResult.source,
821
+ candleSource: candleResult.source
822
+ });
823
+ const startMs = Date.parse(range.start);
824
+ const endMs = Date.parse(range.end);
825
+ const bars = candleResult.candles
826
+ .filter((candle) => {
827
+ const candleStart = Date.parse(candle.start);
828
+ return candleStart >= startMs && candleStart < endMs;
829
+ })
830
+ .sort((left, right) => Date.parse(left.start) - Date.parse(right.start));
831
+ const candleAvailability = bars.length === 0 ? "no_candles_in_range" : "available";
832
+ return {
833
+ status: "ok",
834
+ ...common,
835
+ pair: outputPair,
836
+ candleAvailability,
837
+ barCount: bars.length,
838
+ bars,
839
+ source
840
+ };
841
+ }
842
+ async getDeepbookUsdcPriceAtTime(input) {
843
+ const target = deepbookUsdcPriceAtTimeTarget(input);
844
+ const history = await this.getDeepbookUsdcPriceHistory({
845
+ poolName: input.poolName,
846
+ assetSymbol: input.assetSymbol,
847
+ coinType: input.coinType,
848
+ interval: input.interval,
849
+ start: target.searchWindow.start,
850
+ end: target.searchWindow.end
851
+ });
852
+ const common = {
853
+ fetchedAt: history.fetchedAt,
854
+ target,
855
+ requested: history.requested,
856
+ quantitySemantics: history.quantitySemantics,
857
+ responseSummary: history.responseSummary,
858
+ unsupportedClaims: history.unsupportedClaims
859
+ };
860
+ if (history.status === "unsupported_pair") {
861
+ return {
862
+ status: "unsupported_pair",
863
+ ...common,
864
+ reason: history.reason,
865
+ matchingPoolNames: history.matchingPoolNames,
866
+ availablePoolNames: history.availablePoolNames,
867
+ userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
868
+ };
869
+ }
870
+ if (history.status === "unsupported_range") {
871
+ return {
872
+ status: "unsupported_range",
873
+ ...common,
874
+ reason: history.reason,
875
+ userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
876
+ };
877
+ }
878
+ if (history.status === "source_unavailable") {
879
+ return {
880
+ status: "source_unavailable",
881
+ ...common,
882
+ reason: history.reason,
883
+ pair: history.pair,
884
+ source: history.source,
885
+ userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
886
+ };
887
+ }
888
+ const matched = deepbookUsdcPriceAtTimeMatch({
889
+ target,
890
+ pair: history.pair,
891
+ bars: history.bars
892
+ });
893
+ if (matched === undefined) {
894
+ return {
895
+ status: "no_price_in_search_window",
896
+ ...common,
897
+ pair: history.pair,
898
+ candleAvailability: history.candleAvailability,
899
+ source: history.source,
900
+ userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
901
+ };
902
+ }
903
+ return {
904
+ status: "ok",
905
+ ...common,
906
+ pair: history.pair,
907
+ match: matched.match,
908
+ matchedCandle: matched.candle,
909
+ candleAvailability: history.candleAvailability,
910
+ source: history.source,
911
+ userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(true)
912
+ };
913
+ }
914
+ async quoteDeepbookAction(input) {
915
+ const pool = getKnownPool(input.poolKey);
916
+ const amount = parseRawAmount(input.amountRaw);
917
+ const feeMode = input.feeMode ?? "deep";
918
+ const deepbook = this.#deepbookFactory(input.simulationSender);
919
+ if (feeMode === "input_coin" && (!deepbook.getQuoteQuantityOutInputFeeRaw || !deepbook.getBaseQuantityOutInputFeeRaw)) {
920
+ throw new ReadServiceInputError("quote_unavailable", "Input-fee DeepBook quoting is not supported by this read client", {
921
+ poolKey: input.poolKey
922
+ });
923
+ }
924
+ const rawReturnValues = input.direction === "base_to_quote"
925
+ ? feeMode === "input_coin"
926
+ ? await deepbook.getQuoteQuantityOutInputFeeRaw(input.poolKey, amount)
927
+ : await deepbook.getQuoteQuantityOutRaw(input.poolKey, amount)
928
+ : feeMode === "input_coin"
929
+ ? await deepbook.getBaseQuantityOutInputFeeRaw(input.poolKey, amount)
930
+ : await deepbook.getBaseQuantityOutRaw(input.poolKey, amount);
931
+ const rawQuote = this.#toDeepbookRawQuoteEvidence({
932
+ poolKey: input.poolKey,
933
+ direction: input.direction,
934
+ amountRaw: input.amountRaw,
935
+ rawReturnValues,
936
+ feeMode
937
+ });
938
+ const quote = assertValidDeepbookQuote(input.poolKey, input.direction, toDeepbookDisplayQuoteFromRaw(rawReturnValues, this.#deepbookQuoteUnits(pool.baseCoin, pool.quoteCoin)));
939
+ return {
940
+ status: "ok",
941
+ poolKey: input.poolKey,
942
+ direction: input.direction,
943
+ amountRaw: input.amountRaw,
944
+ fetchedAt: this.#fetchedAt(),
945
+ userAnswerUse: deepbookQuoteUserAnswerUse("raw"),
946
+ quantitySemantics: deepbookQuoteQuantitySemantics("raw_u64"),
947
+ source: {
948
+ ...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
949
+ method: input.direction === "base_to_quote"
950
+ ? feeMode === "input_coin"
951
+ ? "getQuoteQuantityOutInputFee"
952
+ : "getQuoteQuantityOut"
953
+ : feeMode === "input_coin"
954
+ ? "getBaseQuantityOutInputFee"
955
+ : "getBaseQuantityOut",
956
+ returnValueEncoding: "bcs.u64"
957
+ },
958
+ quote,
959
+ rawQuote
960
+ };
961
+ }
962
+ async quoteDeepbookDisplayAmount(input) {
963
+ const pool = getKnownPool(input.poolKey);
964
+ const sourceSymbol = input.direction === "base_to_quote" ? pool.baseCoin : pool.quoteCoin;
965
+ const sourceCoin = getDeepbookCoinEntryBySymbol(sourceSymbol, this.#deepbookCoins);
966
+ const decimals = decimalsFromScalar(sourceCoin.coin.scalar);
967
+ if (decimals === undefined) {
968
+ throw invalidDeepbookScalar(sourceCoin.symbol, sourceCoin.coin.scalar);
969
+ }
970
+ const amountRaw = parseQuoteDisplayAmount(input.amountDisplay, decimals);
971
+ const rawQuote = await this.quoteDeepbookAction({
972
+ poolKey: input.poolKey,
973
+ direction: input.direction,
974
+ amountRaw,
975
+ simulationSender: input.simulationSender,
976
+ feeMode: input.feeMode
977
+ });
978
+ return {
979
+ status: "ok",
980
+ pool: {
981
+ poolKey: input.poolKey,
982
+ base: pool.baseCoin,
983
+ quote: pool.quoteCoin
984
+ },
985
+ direction: input.direction,
986
+ inputAmount: {
987
+ display: input.amountDisplay,
988
+ raw: amountRaw,
989
+ asset: {
990
+ symbol: sourceCoin.symbol,
991
+ coinType: normalizeCoinType(sourceCoin.coin.type),
992
+ decimals,
993
+ unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
994
+ }
995
+ },
996
+ fetchedAt: rawQuote.fetchedAt,
997
+ userAnswerUse: deepbookQuoteUserAnswerUse("display"),
998
+ quantitySemantics: deepbookQuoteQuantitySemantics("display_source_amount_converted_to_raw_u64"),
999
+ source: rawQuote.source,
1000
+ quote: rawQuote.quote,
1001
+ rawQuote: rawQuote.rawQuote
1002
+ };
1003
+ }
1004
+ #toDeepbookRawQuoteEvidence(input) {
1005
+ const pool = getKnownPool(input.poolKey);
1006
+ const inputSymbol = input.direction === "base_to_quote" ? pool.baseCoin : pool.quoteCoin;
1007
+ const outputSymbol = input.direction === "base_to_quote" ? pool.quoteCoin : pool.baseCoin;
1008
+ const baseOut = this.#deepbookRawQuoteAmount(pool.baseCoin, input.rawReturnValues.baseOutRaw);
1009
+ const quoteOut = this.#deepbookRawQuoteAmount(pool.quoteCoin, input.rawReturnValues.quoteOutRaw);
1010
+ return {
1011
+ kind: DEEPBOOK_RAW_QUOTE_QUANTITY_KIND,
1012
+ sourceMoveFunction: input.direction === "base_to_quote"
1013
+ ? input.feeMode === "input_coin"
1014
+ ? "pool::get_quote_quantity_out_input_fee"
1015
+ : "pool::get_quote_quantity_out"
1016
+ : input.feeMode === "input_coin"
1017
+ ? "pool::get_base_quantity_out_input_fee"
1018
+ : "pool::get_base_quantity_out",
1019
+ returnValueSourceMoveFunction: input.feeMode === "input_coin" ? "pool::get_quantity_out_input_fee" : "pool::get_quantity_out",
1020
+ returnValueOrder: ["base_quantity_out", "quote_quantity_out", "deep_quantity_required"],
1021
+ inputAmount: this.#deepbookRawQuoteAmount(inputSymbol, input.amountRaw),
1022
+ baseOut,
1023
+ quoteOut,
1024
+ deepRequired: this.#deepbookRawQuoteAmount("DEEP", input.rawReturnValues.deepRequiredRaw),
1025
+ directionalOutput: outputSymbol === pool.baseCoin ? baseOut : quoteOut,
1026
+ boundary: {
1027
+ outputBeforeSlippagePolicy: true,
1028
+ notFor: [
1029
+ "final_min_out",
1030
+ "transaction_building",
1031
+ "signing_data",
1032
+ "signing_readiness",
1033
+ "price_impact",
1034
+ "mid_price_slippage",
1035
+ "quote_vs_mid_slippage",
1036
+ "effective_price",
1037
+ "venue_comparison",
1038
+ "best_route",
1039
+ "route_recommendation",
1040
+ "fiat_usd_cash_out",
1041
+ "external_market_price_conversion",
1042
+ "external_market_lookup",
1043
+ "usd_peg_assumption",
1044
+ "bank_cash_out_estimate",
1045
+ "profit_or_pnl",
1046
+ "cost_basis"
1047
+ ]
1048
+ }
1049
+ };
1050
+ }
1051
+ #deepbookRawQuoteAmount(symbol, raw) {
1052
+ const coin = getDeepbookCoinEntryBySymbol(symbol, this.#deepbookCoins);
1053
+ const decimals = decimalsFromScalar(coin.coin.scalar);
1054
+ if (decimals === undefined) {
1055
+ throw invalidDeepbookScalar(coin.symbol, coin.coin.scalar);
1056
+ }
1057
+ return {
1058
+ raw,
1059
+ symbol: coin.symbol,
1060
+ coinType: normalizeCoinType(coin.coin.type),
1061
+ decimals,
1062
+ unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
1063
+ };
1064
+ }
1065
+ #deepbookQuoteUnits(baseSymbol, quoteSymbol) {
1066
+ const baseCoin = getDeepbookCoinEntryBySymbol(baseSymbol, this.#deepbookCoins);
1067
+ const quoteCoin = getDeepbookCoinEntryBySymbol(quoteSymbol, this.#deepbookCoins);
1068
+ const deepCoin = getDeepbookCoinEntryBySymbol("DEEP", this.#deepbookCoins);
1069
+ const baseDecimals = decimalsFromScalar(baseCoin.coin.scalar);
1070
+ const quoteDecimals = decimalsFromScalar(quoteCoin.coin.scalar);
1071
+ const deepDecimals = decimalsFromScalar(deepCoin.coin.scalar);
1072
+ if (baseDecimals === undefined) {
1073
+ throw invalidDeepbookScalar(baseCoin.symbol, baseCoin.coin.scalar);
1074
+ }
1075
+ if (quoteDecimals === undefined) {
1076
+ throw invalidDeepbookScalar(quoteCoin.symbol, quoteCoin.coin.scalar);
1077
+ }
1078
+ if (deepDecimals === undefined) {
1079
+ throw invalidDeepbookScalar(deepCoin.symbol, deepCoin.coin.scalar);
1080
+ }
1081
+ return {
1082
+ baseDecimals,
1083
+ quoteDecimals,
1084
+ deepDecimals
1085
+ };
1086
+ }
1087
+ async summarizeDeepbookAccountInventory(input) {
1088
+ const normalizedManagerAddress = normalizeOptionalManagerAddress(input.managerAddress);
1089
+ const pool = input.poolKey === undefined ? undefined : getKnownPool(input.poolKey);
1090
+ const discoveryClient = this.#deepbookFactory(input.account);
1091
+ const managerAddresses = normalizeManagerAddresses(await discoveryClient.getBalanceManagerIds(input.account));
1092
+ const requested = {
1093
+ ...(input.poolKey === undefined ? {} : { poolKey: input.poolKey }),
1094
+ ...(normalizedManagerAddress === undefined ? {} : { managerAddress: normalizedManagerAddress })
1095
+ };
1096
+ const base = {
1097
+ status: "ok",
1098
+ account: input.account,
1099
+ fetchedAt: this.#fetchedAt(),
1100
+ requested,
1101
+ managerAddresses,
1102
+ quantitySemantics: deepbookDisplayQuantitySemantics(),
1103
+ ...(input.poolKey === undefined || pool === undefined
1104
+ ? {}
1105
+ : {
1106
+ pool: {
1107
+ poolKey: input.poolKey,
1108
+ base: pool.baseCoin,
1109
+ quote: pool.quoteCoin
1110
+ }
1111
+ })
1112
+ };
1113
+ if (input.poolKey === undefined && normalizedManagerAddress === undefined) {
1114
+ return {
1115
+ ...base,
1116
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_discovery_only"),
1117
+ detailStatus: "manager_discovery_only",
1118
+ source: deepbookAccountInventorySource(["getBalanceManagerIds"])
1119
+ };
1120
+ }
1121
+ if (input.poolKey === undefined) {
1122
+ return {
1123
+ ...base,
1124
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("pool_key_required"),
1125
+ detailStatus: "pool_key_required",
1126
+ source: deepbookAccountInventorySource(["getBalanceManagerIds"])
1127
+ };
1128
+ }
1129
+ if (normalizedManagerAddress === undefined) {
1130
+ return {
1131
+ ...base,
1132
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_address_required"),
1133
+ detailStatus: "manager_address_required",
1134
+ source: deepbookAccountInventorySource(["getBalanceManagerIds"])
1135
+ };
1136
+ }
1137
+ if (!managerAddresses.includes(normalizedManagerAddress)) {
1138
+ return {
1139
+ ...base,
1140
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_address_not_discovered_for_active_account"),
1141
+ detailStatus: "manager_address_not_discovered_for_active_account",
1142
+ source: deepbookAccountInventorySource(["getBalanceManagerIds"])
1143
+ };
1144
+ }
1145
+ const detailClient = this.#deepbookFactory(input.account, {
1146
+ balanceManagers: {
1147
+ [normalizedManagerAddress]: { address: normalizedManagerAddress }
1148
+ }
1149
+ });
1150
+ const accountExists = await detailClient.accountExists(input.poolKey, normalizedManagerAddress);
1151
+ if (!accountExists) {
1152
+ return {
1153
+ ...base,
1154
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("account_not_found"),
1155
+ detailStatus: "account_not_found",
1156
+ source: deepbookAccountInventorySource(["getBalanceManagerIds", "accountExists"]),
1157
+ accountExists
1158
+ };
1159
+ }
1160
+ const [accountSummary, lockedBalances, openOrderIds] = await Promise.all([
1161
+ detailClient.account(input.poolKey, normalizedManagerAddress),
1162
+ detailClient.lockedBalance(input.poolKey, normalizedManagerAddress),
1163
+ detailClient.accountOpenOrders(input.poolKey, normalizedManagerAddress)
1164
+ ]);
1165
+ const cappedOpenOrderIds = openOrderIds.slice(0, MAX_DEEPBOOK_ACCOUNT_OPEN_ORDER_IDS);
1166
+ return {
1167
+ ...base,
1168
+ userAnswerUse: deepbookAccountInventoryUserAnswerUse("available"),
1169
+ detailStatus: "available",
1170
+ source: deepbookAccountInventorySource([
1171
+ "getBalanceManagerIds",
1172
+ "accountExists",
1173
+ "account",
1174
+ "lockedBalance",
1175
+ "accountOpenOrders"
1176
+ ]),
1177
+ accountExists,
1178
+ accountSummary: toDeepbookAccountSummary(accountSummary),
1179
+ lockedBalances: assertDeepbookDisplayBalances(lockedBalances, "lockedBalances"),
1180
+ openOrderIds: cappedOpenOrderIds,
1181
+ openOrderCount: openOrderIds.length,
1182
+ openOrderIdsTruncated: openOrderIds.length > cappedOpenOrderIds.length
1183
+ };
1184
+ }
1185
+ #resolveSettlementAssetGroupSymbol(symbol, assetGroup, field) {
1186
+ const canonical = canonicalDeepbookSymbol(symbol, this.#deepbookCoins);
1187
+ if (canonical === undefined) {
1188
+ throw new ReadServiceInputError("input_invalid", "Settlement asset symbol is not in the pinned DeepBook registry", {
1189
+ field,
1190
+ value: symbol
1191
+ });
1192
+ }
1193
+ const asset = assetGroup.includedAssets.find((candidate) => candidate.symbol === canonical);
1194
+ if (asset === undefined) {
1195
+ throw new ReadServiceInputError("input_invalid", "Settlement asset symbol is not in the supported assetGroup", {
1196
+ field,
1197
+ value: symbol,
1198
+ canonicalSymbol: canonical,
1199
+ assetGroupId: assetGroup.id
1200
+ });
1201
+ }
1202
+ return asset;
1203
+ }
1204
+ #intentEvidenceAssetGroupBalances(assetGroup, classifiedAssets) {
1205
+ return assetGroup.includedAssets.map((asset) => {
1206
+ const matchingBalances = classifiedAssets
1207
+ .filter((classified) => {
1208
+ try {
1209
+ return normalizeCoinType(classified.balance.coinType) === asset.coinType;
1210
+ }
1211
+ catch {
1212
+ return false;
1213
+ }
1214
+ })
1215
+ .map((classified) => classified.balance.balance);
1216
+ const currentRawAmount = sumRawAmounts(matchingBalances);
1217
+ return {
1218
+ ...asset,
1219
+ currentRawAmount,
1220
+ currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, asset.decimals),
1221
+ walletBalanceEvidence: "current_wallet_coin_balance_snapshot"
1222
+ };
1223
+ });
1224
+ }
1225
+ #intentEvidenceAggregate(input) {
1226
+ if (input.blockedReason !== undefined) {
1227
+ return {
1228
+ status: "unavailable_wallet_balance_scan_incomplete",
1229
+ ...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount }),
1230
+ reason: input.blockedReason
1231
+ };
1232
+ }
1233
+ if (input.commonDecimals === undefined) {
1234
+ return {
1235
+ status: "unavailable_mixed_decimals",
1236
+ ...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount })
1237
+ };
1238
+ }
1239
+ const currentRawAmount = sumRawAmounts(input.balances.map((balance) => balance.currentRawAmount));
1240
+ if (input.requiredDisplayAmount === undefined) {
1241
+ return {
1242
+ status: "available",
1243
+ currentRawAmount,
1244
+ currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.commonDecimals),
1245
+ decimals: input.commonDecimals,
1246
+ unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
1247
+ };
1248
+ }
1249
+ const requiredRawAmount = this.#parseIntentDisplayAmount(input.requiredDisplayAmount, input.commonDecimals, "requiredDisplayAmount");
1250
+ const shortfallRawAmount = BigInt(currentRawAmount) >= BigInt(requiredRawAmount)
1251
+ ? "0"
1252
+ : (BigInt(requiredRawAmount) - BigInt(currentRawAmount)).toString();
1253
+ return {
1254
+ status: "available",
1255
+ requiredDisplayAmount: input.requiredDisplayAmount,
1256
+ requiredRawAmount,
1257
+ currentRawAmount,
1258
+ currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.commonDecimals),
1259
+ shortfallRawAmount,
1260
+ shortfallDisplayAmount: formatSettlementAssetRawAmount(shortfallRawAmount, input.commonDecimals),
1261
+ decimals: input.commonDecimals,
1262
+ unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
1263
+ };
1264
+ }
1265
+ #intentEvidenceSettlementAssetCoverage(aggregate) {
1266
+ const boundary = intentEvidenceSettlementAssetCoverageBoundary();
1267
+ if (aggregate.status === "unavailable_mixed_decimals") {
1268
+ return {
1269
+ status: "unavailable_mixed_decimals",
1270
+ ...(aggregate.requiredDisplayAmount === undefined
1271
+ ? {}
1272
+ : { requiredDisplayAmount: aggregate.requiredDisplayAmount }),
1273
+ reason: "asset_group_assets_do_not_share_verified_decimals",
1274
+ boundary
1275
+ };
1276
+ }
1277
+ if (aggregate.status === "unavailable_wallet_balance_scan_incomplete") {
1278
+ return {
1279
+ status: "unavailable_wallet_balance_scan_incomplete",
1280
+ ...(aggregate.requiredDisplayAmount === undefined
1281
+ ? {}
1282
+ : { requiredDisplayAmount: aggregate.requiredDisplayAmount }),
1283
+ reason: aggregate.reason,
1284
+ boundary
1285
+ };
1286
+ }
1287
+ if (aggregate.requiredDisplayAmount === undefined) {
1288
+ return {
1289
+ status: "balance_total_only",
1290
+ currentRawAmount: aggregate.currentRawAmount,
1291
+ currentDisplayAmount: aggregate.currentDisplayAmount,
1292
+ decimals: aggregate.decimals,
1293
+ unitSource: aggregate.unitSource,
1294
+ boundary
1295
+ };
1296
+ }
1297
+ if (aggregate.requiredRawAmount === undefined ||
1298
+ aggregate.shortfallRawAmount === undefined ||
1299
+ aggregate.shortfallDisplayAmount === undefined) {
1300
+ throw new Error("settlement-asset coverage requires complete target amount evidence");
1301
+ }
1302
+ const shortfallRawAmount = aggregate.shortfallRawAmount;
1303
+ return {
1304
+ status: BigInt(shortfallRawAmount) === 0n ? "covered_by_settlement_asset_balance" : "shortfall_in_settlement_asset_balance",
1305
+ requiredDisplayAmount: aggregate.requiredDisplayAmount,
1306
+ requiredRawAmount: aggregate.requiredRawAmount,
1307
+ currentRawAmount: aggregate.currentRawAmount,
1308
+ currentDisplayAmount: aggregate.currentDisplayAmount,
1309
+ shortfallRawAmount,
1310
+ shortfallDisplayAmount: aggregate.shortfallDisplayAmount,
1311
+ decimals: aggregate.decimals,
1312
+ unitSource: aggregate.unitSource,
1313
+ boundary
1314
+ };
1315
+ }
1316
+ #intentEvidenceSelectedTarget(input) {
1317
+ const targetBalance = input.balances.find((balance) => balance.symbol === input.targetAsset.symbol);
1318
+ const currentRawAmount = targetBalance?.currentRawAmount ?? "0";
1319
+ const requiredRawAmount = this.#parseIntentDisplayAmount(input.requiredDisplayAmount, input.targetAsset.decimals, "requiredDisplayAmount");
1320
+ const shortfallRawAmount = BigInt(currentRawAmount) >= BigInt(requiredRawAmount)
1321
+ ? "0"
1322
+ : (BigInt(requiredRawAmount) - BigInt(currentRawAmount)).toString();
1323
+ return {
1324
+ ...input.targetAsset,
1325
+ selectionSource: input.selectionSource,
1326
+ requiredRawAmount,
1327
+ currentRawAmount,
1328
+ currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.targetAsset.decimals),
1329
+ shortfallRawAmount,
1330
+ shortfallDisplayAmount: formatSettlementAssetRawAmount(shortfallRawAmount, input.targetAsset.decimals)
1331
+ };
1332
+ }
1333
+ async #settlementAssetGroupParityAsset(asset, referenceAsset, deepbook) {
1334
+ if (asset.symbol === referenceAsset.symbol) {
1335
+ return {
1336
+ ...asset,
1337
+ status: "reference_asset",
1338
+ priceInReferenceAsset: 1,
1339
+ parityDirection: "reference_asset_per_group_asset",
1340
+ reason: "reference_asset_is_measurement_baseline_not_settlement_choice"
1341
+ };
1342
+ }
1343
+ let directPool;
1344
+ try {
1345
+ const resolved = resolveDeepbookPoolForSymbols({
1346
+ sourceSymbol: asset.symbol,
1347
+ targetSymbol: referenceAsset.symbol
1348
+ });
1349
+ directPool = { poolKey: resolved.poolKey, direction: resolved.direction };
1350
+ }
1351
+ catch {
1352
+ return {
1353
+ ...asset,
1354
+ status: "no_direct_deepbook_pool",
1355
+ reason: "No direct DeepBook mainnet pool exists between this group asset and the measurement reference asset."
1356
+ };
1357
+ }
1358
+ let poolMidPrice;
1359
+ try {
1360
+ poolMidPrice = assertValidDeepbookMidPrice(directPool.poolKey, await deepbook.midPrice(directPool.poolKey));
1361
+ }
1362
+ catch (error) {
1363
+ return {
1364
+ ...asset,
1365
+ status: "mid_price_unavailable",
1366
+ poolKey: directPool.poolKey,
1367
+ direction: directPool.direction,
1368
+ reason: error instanceof Error ? error.message : "DeepBook mid-price lookup failed."
1369
+ };
1370
+ }
1371
+ return {
1372
+ ...asset,
1373
+ status: "measured",
1374
+ priceInReferenceAsset: roundPriceInReferenceAsset(directPool.direction === "base_to_quote" ? poolMidPrice : 1 / poolMidPrice),
1375
+ parityDirection: "reference_asset_per_group_asset",
1376
+ poolKey: directPool.poolKey,
1377
+ direction: directPool.direction,
1378
+ poolMidPrice,
1379
+ poolMidPriceDirection: DEEPBOOK_MID_PRICE_DIRECTION
1380
+ };
1381
+ }
1382
+ async #intentEvidenceCandidateConversions(input) {
1383
+ const candidates = [];
1384
+ for (const balance of input.balances) {
1385
+ if (balance.currentRawAmount === "0" || balance.symbol === input.targetAsset?.symbol) {
1386
+ continue;
1387
+ }
1388
+ if (input.acceptedSourceSet !== undefined && !input.acceptedSourceSet.has(balance.symbol)) {
1389
+ candidates.push({
1390
+ sourceSymbol: balance.symbol,
1391
+ ...(input.targetAsset === undefined ? {} : { targetSymbol: input.targetAsset.symbol }),
1392
+ sourceRawAmount: balance.currentRawAmount,
1393
+ sourceDisplayAmount: balance.currentDisplayAmount,
1394
+ status: "filtered_by_accepted_source_assets",
1395
+ reason: "The source asset was not included in acceptedSourceAssetSymbols."
1396
+ });
1397
+ continue;
1398
+ }
1399
+ if (input.targetAsset === undefined) {
1400
+ candidates.push({
1401
+ sourceSymbol: balance.symbol,
1402
+ sourceRawAmount: balance.currentRawAmount,
1403
+ sourceDisplayAmount: balance.currentDisplayAmount,
1404
+ status: "target_asset_not_selected",
1405
+ reason: "No target settlement asset was selected, so conversion quotes are not requested."
1406
+ });
1407
+ continue;
1408
+ }
1409
+ let directPool;
1410
+ try {
1411
+ const resolved = resolveDeepbookPoolForSymbols({
1412
+ sourceSymbol: balance.symbol,
1413
+ targetSymbol: input.targetAsset.symbol
1414
+ });
1415
+ directPool = { poolKey: resolved.poolKey, direction: resolved.direction };
1416
+ }
1417
+ catch {
1418
+ candidates.push({
1419
+ sourceSymbol: balance.symbol,
1420
+ targetSymbol: input.targetAsset.symbol,
1421
+ sourceRawAmount: balance.currentRawAmount,
1422
+ sourceDisplayAmount: balance.currentDisplayAmount,
1423
+ status: "no_direct_deepbook_pool",
1424
+ reason: "No direct DeepBook mainnet pool exists for this source and target pair."
1425
+ });
1426
+ continue;
1427
+ }
1428
+ try {
1429
+ const quote = await this.quoteDeepbookAction({
1430
+ poolKey: directPool.poolKey,
1431
+ direction: directPool.direction,
1432
+ amountRaw: balance.currentRawAmount,
1433
+ simulationSender: DEFAULT_DEEPBOOK_SIMULATION_SENDER
1434
+ });
1435
+ candidates.push({
1436
+ sourceSymbol: balance.symbol,
1437
+ targetSymbol: input.targetAsset.symbol,
1438
+ sourceRawAmount: balance.currentRawAmount,
1439
+ sourceDisplayAmount: balance.currentDisplayAmount,
1440
+ status: "quoted",
1441
+ directPool,
1442
+ quote,
1443
+ boundary: [
1444
+ "quote_snapshot_only",
1445
+ "not_final_min_out",
1446
+ "not_route_recommendation",
1447
+ "not_route_dependent_payment_support",
1448
+ "not_payment_readiness",
1449
+ "not_signing_readiness"
1450
+ ]
1451
+ });
1452
+ }
1453
+ catch (error) {
1454
+ candidates.push({
1455
+ sourceSymbol: balance.symbol,
1456
+ targetSymbol: input.targetAsset.symbol,
1457
+ sourceRawAmount: balance.currentRawAmount,
1458
+ sourceDisplayAmount: balance.currentDisplayAmount,
1459
+ status: "quote_unavailable",
1460
+ reason: error instanceof Error ? error.message : "DeepBook quote failed for this candidate.",
1461
+ directPool
1462
+ });
1463
+ }
1464
+ }
1465
+ return candidates;
1466
+ }
1467
+ #intentEvidenceRequiredUserChoices(intentKind, targetAsset, candidateConversions) {
1468
+ if (intentKind === "summarize_settlement_asset_group_balance") {
1469
+ return [];
1470
+ }
1471
+ const choices = [];
1472
+ if (targetAsset === undefined) {
1473
+ choices.push("Choose the onchain settlement asset or merchant-accepted USD-denominated asset set before target-specific settlement evidence can be completed.");
1474
+ }
1475
+ if (candidateConversions.some((candidate) => candidate.status === "quoted")) {
1476
+ choices.push("Choose which quoted candidate assets, if any, the user wants to convert.");
1477
+ }
1478
+ return choices;
1479
+ }
1480
+ #parseIntentDisplayAmount(displayAmount, decimals, field) {
1481
+ try {
1482
+ return parseDisplayAmountToRaw(displayAmount, decimals);
1483
+ }
1484
+ catch (error) {
1485
+ throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount must be an unsigned decimal string within verified decimals", {
1486
+ field,
1487
+ value: displayAmount,
1488
+ decimals,
1489
+ reason: error instanceof Error ? error.message : "unknown"
1490
+ });
1491
+ }
1492
+ }
1493
+ #fetchedAt() {
1494
+ return this.#now().toISOString();
1495
+ }
1496
+ // Resolve one coin type's verified unit (decimals/symbol) through the shared
1497
+ // coin-metadata cache and DeepBook fallback. Public so other readers (e.g. the
1498
+ // public chain-receipt reader) can render decimal amounts without duplicating
1499
+ // the cache logic. Fails soft to an unavailable unit on an unresolvable type.
1500
+ async resolveCoinUnit(coinType) {
1501
+ let normalizedCoinType;
1502
+ try {
1503
+ normalizedCoinType = normalizeCoinType(coinType);
1504
+ }
1505
+ catch {
1506
+ return unavailableUnit("coin_type_unresolved");
1507
+ }
1508
+ return this.#resolveCoinUnitForNormalizedCoinType(normalizedCoinType);
1509
+ }
1510
+ async #withUnit(balance) {
1511
+ let normalizedCoinType;
1512
+ try {
1513
+ normalizedCoinType = normalizeCoinType(balance.coinType);
1514
+ }
1515
+ catch {
1516
+ return withUnavailableUnit(balance, "coin_type_unresolved");
1517
+ }
1518
+ const unit = await this.#resolveCoinUnitForNormalizedCoinType(normalizedCoinType);
1519
+ return withResolvedUnit(balance, unit);
1520
+ }
1521
+ async #resolveCoinUnitForNormalizedCoinType(normalizedCoinType) {
1522
+ const now = this.#now();
1523
+ let cached;
1524
+ try {
1525
+ cached = await this.#coinMetadataCache.getCoinMetadata({
1526
+ coinType: normalizedCoinType,
1527
+ chainIdentifier: this.#chainIdentifier,
1528
+ now
1529
+ });
1530
+ }
1531
+ catch (error) {
1532
+ throw new ReadServiceCacheError("read", error);
1533
+ }
1534
+ if (cached.status === "hit") {
1535
+ return unitFromMetadataRecord(cached.record, "hit");
1536
+ }
1537
+ let metadata;
1538
+ try {
1539
+ metadata = await this.#client.core.getCoinMetadata({ coinType: normalizedCoinType });
1540
+ }
1541
+ catch {
1542
+ return unavailableUnit("metadata_lookup_failed");
1543
+ }
1544
+ if (metadata.coinMetadata !== null) {
1545
+ const record = this.#cacheRecordFromMetadata(normalizedCoinType, metadata.coinMetadata, now);
1546
+ try {
1547
+ await this.#coinMetadataCache.setCoinMetadata(record);
1548
+ }
1549
+ catch (error) {
1550
+ throw new ReadServiceCacheError("write", error);
1551
+ }
1552
+ return unitFromMetadataRecord(record, cached.status === "expired" ? "expired_refetched" : "miss");
1553
+ }
1554
+ try {
1555
+ const fallback = deepbookUnitForCoinType(normalizedCoinType, this.#deepbookCoins);
1556
+ if (fallback) {
1557
+ return unitFromDeepbook(fallback);
1558
+ }
1559
+ }
1560
+ catch (error) {
1561
+ if (error instanceof ReadServiceInputError) {
1562
+ return unavailableUnit("no_verified_decimals");
1563
+ }
1564
+ throw error;
1565
+ }
1566
+ return unavailableUnit("metadata_not_found");
1567
+ }
1568
+ async #scanWalletAssetClassificationPages(account) {
1569
+ const classifiedAssets = [];
1570
+ let uninspectedAssetClasses = NOT_INSPECTED_ASSET_CLASSES.map((assetClass) => ({
1571
+ ...assetClass
1572
+ }));
1573
+ let cursor;
1574
+ const requestedCursors = new Set();
1575
+ for (let pageIndex = 0; pageIndex < MAX_WALLET_BALANCE_SCAN_PAGES; pageIndex += 1) {
1576
+ if (cursor !== undefined && cursor !== null) {
1577
+ if (requestedCursors.has(cursor)) {
1578
+ return {
1579
+ classifiedAssets,
1580
+ uninspectedAssetClasses,
1581
+ inspectedBalancePages: pageIndex,
1582
+ inspectedCoinBalanceCount: classifiedAssets.length,
1583
+ blockedReason: "wallet_balance_pagination_did_not_advance"
1584
+ };
1585
+ }
1586
+ requestedCursors.add(cursor);
1587
+ }
1588
+ const page = await this.classifyWalletAssets({ account, ...(cursor === undefined ? {} : { cursor }) });
1589
+ classifiedAssets.push(...page.classifiedAssets);
1590
+ uninspectedAssetClasses = page.uninspectedAssetClasses;
1591
+ if (!page.hasNextPage) {
1592
+ return {
1593
+ classifiedAssets,
1594
+ uninspectedAssetClasses,
1595
+ inspectedBalancePages: pageIndex + 1,
1596
+ inspectedCoinBalanceCount: classifiedAssets.length
1597
+ };
1598
+ }
1599
+ if (typeof page.cursor !== "string" ||
1600
+ page.cursor.length === 0 ||
1601
+ page.cursor === cursor ||
1602
+ requestedCursors.has(page.cursor)) {
1603
+ return {
1604
+ classifiedAssets,
1605
+ uninspectedAssetClasses,
1606
+ inspectedBalancePages: pageIndex + 1,
1607
+ inspectedCoinBalanceCount: classifiedAssets.length,
1608
+ blockedReason: "wallet_balance_pagination_did_not_advance"
1609
+ };
1610
+ }
1611
+ cursor = page.cursor;
1612
+ }
1613
+ return {
1614
+ classifiedAssets,
1615
+ uninspectedAssetClasses,
1616
+ inspectedBalancePages: MAX_WALLET_BALANCE_SCAN_PAGES,
1617
+ inspectedCoinBalanceCount: classifiedAssets.length,
1618
+ blockedReason: "wallet_balance_page_limit_exceeded"
1619
+ };
1620
+ }
1621
+ #cacheRecordFromMetadata(coinType, metadata, now) {
1622
+ const decimals = assertValidDecimals(metadata.decimals);
1623
+ return {
1624
+ coinType,
1625
+ chainIdentifier: this.#chainIdentifier,
1626
+ decimals,
1627
+ symbol: metadata.symbol,
1628
+ name: metadata.name,
1629
+ fetchedAt: now.toISOString(),
1630
+ expiresAt: new Date(now.getTime() + this.#coinMetadataTtlMs).toISOString()
1631
+ };
1632
+ }
1633
+ listDeepbookTokenRegistry() {
1634
+ return listDeepbookTokenRegistry(this.#deepbookCoins);
1635
+ }
1636
+ }
1637
+ export function createSuiReadService(options) {
1638
+ return new SuiReadService(options);
1639
+ }